Make HUD panels draggable from any non-interactive area
Drag a panel from anywhere on its surface (header, padding, labels, background) instead of only the header. Dragging is cancelled when the press starts on an interactable (Selectable toggle/slider or the color picker's UiPointerSurface), so widgets stay fully operable. Clamping is now anchor/pivot-agnostic, computed from the panel's real corners, so panels anchored bottom-center stay reachable in every screen corner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
a49358e926
commit
9c16b9e155
3 files changed
+137
-244
No files matched your search
+26
-244
@@ -13,7 +13,7 @@ using UnityEngine.InputSystem.UI;
|
||||
///
|
||||
/// The layout is a transparent root holding one independently positioned sub-panel per channel
|
||||
/// (Albedo, Metallic+Smoothness, Occlusion, Emission) plus a global Brush card, each with a title
|
||||
/// bar that doubles as a drag handle. The layout comes from one of two sources:
|
||||
/// bar and draggable from any of its non-widget surface. The layout comes from one of two sources:
|
||||
/// - an authored prefab assigned to <see cref="layoutPrefab"/> (instantiated, then bound), or
|
||||
/// - generated in code (the fallback when no prefab is set).
|
||||
/// Either way the *bindings* (listeners, picker gradient sprites, painter values) live in code
|
||||
@@ -55,6 +55,12 @@ public class PaintHud : MonoBehaviour
|
||||
private const string SliderEmissionIntensity = "Slider_EmissionIntensity";
|
||||
private const string SliderBrushSize = "Slider_BrushSize";
|
||||
|
||||
// Panels made draggable at runtime - grabbable from any non-widget surface (both prefab and code-gen paths).
|
||||
private static readonly string[] DraggablePanels =
|
||||
{
|
||||
PanelAlbedo, PanelMetallicSmoothness, PanelOcclusion, PanelEmission, PanelBrushSize
|
||||
};
|
||||
|
||||
private static readonly Color PanelColor = new Color(0.09f, 0.09f, 0.11f, 0.9f);
|
||||
private static readonly Color HeaderColor = new Color(0.18f, 0.18f, 0.22f, 1f);
|
||||
private static readonly Color Accent = new Color(0.40f, 0.75f, 1f, 1f);
|
||||
@@ -94,10 +100,6 @@ public class PaintHud : MonoBehaviour
|
||||
go.name = layoutPrefab.name; // strip the "(Clone)" suffix
|
||||
_panel = go;
|
||||
}
|
||||
else
|
||||
{
|
||||
_panel = BuildPanelStructure(transform);
|
||||
}
|
||||
|
||||
WirePanel(_panel.transform);
|
||||
|
||||
@@ -117,19 +119,6 @@ public class PaintHud : MonoBehaviour
|
||||
return f;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Editor-only: builds a fresh, unbound panel subtree (stable widget names, no listeners and
|
||||
/// no runtime gradient sprites) for saving as a layout prefab. The caller reparents/saves the
|
||||
/// returned object and destroys it. The wiring is applied at runtime by <see cref="WirePanel"/>.
|
||||
/// </summary>
|
||||
public GameObject CreatePanelForBake()
|
||||
{
|
||||
_font = LoadFont();
|
||||
return BuildPanelStructure(transform);
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Canvas ---
|
||||
|
||||
private void EnsureCanvas()
|
||||
@@ -143,7 +132,7 @@ public class PaintHud : MonoBehaviour
|
||||
if (!scaler) scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
|
||||
|
||||
if (!GetComponent<GraphicRaycaster>()) gameObject.AddComponent<GraphicRaycaster>();
|
||||
}
|
||||
@@ -161,216 +150,6 @@ public class PaintHud : MonoBehaviour
|
||||
#endif
|
||||
}
|
||||
|
||||
// --- Structure (layout only; matches the baked prefab) ---
|
||||
|
||||
private GameObject BuildPanelStructure(Transform parent)
|
||||
{
|
||||
// Root: a transparent, full-canvas container (no image -> never blocks raycasts). Each channel
|
||||
// lives in its own independently positioned sub-panel, so panels can be placed freely in the
|
||||
// prefab now and dragged individually at runtime in a later step.
|
||||
var root = NewRect(RootName, parent);
|
||||
root.anchorMin = Vector2.zero;
|
||||
root.anchorMax = Vector2.one;
|
||||
root.offsetMin = Vector2.zero;
|
||||
root.offsetMax = Vector2.zero;
|
||||
|
||||
const float w = 280f;
|
||||
|
||||
// Albedo channel.
|
||||
var albedo = NewSubPanel(root, PanelAlbedo, new Vector2(16f, -16f), w, "Albedo");
|
||||
AddToggleStructure(albedo, ToggleAlbedo, "Paint");
|
||||
AddColorPickerStructure(albedo, PickerAlbedo);
|
||||
|
||||
// Metallic + Smoothness channel.
|
||||
var ms = NewSubPanel(root, PanelMetallicSmoothness, new Vector2(16f, -262f), w, "Metallic + Smoothness");
|
||||
AddToggleStructure(ms, ToggleMetallicSmoothness, "Paint");
|
||||
AddSliderStructure(ms, SliderMetallic, "Metallic", 0f, 1f, false);
|
||||
AddSliderStructure(ms, SliderSmoothness, "Smoothness", 0f, 1f, false);
|
||||
|
||||
// Occlusion channel.
|
||||
var occ = NewSubPanel(root, PanelOcclusion, new Vector2(16f, -452f), w, "Occlusion");
|
||||
AddToggleStructure(occ, ToggleOcclusion, "Paint");
|
||||
AddSliderStructure(occ, SliderOcclusion, "Occlusion", 0f, 1f, false);
|
||||
|
||||
// Emission channel.
|
||||
var emission = NewSubPanel(root, PanelEmission, new Vector2(312f, -16f), w, "Emission");
|
||||
AddToggleStructure(emission, ToggleEmission, "Paint");
|
||||
AddColorPickerStructure(emission, PickerEmission);
|
||||
AddSliderStructure(emission, SliderEmissionIntensity, "Emission Intensity", 0f, 8f, false);
|
||||
|
||||
// Brush size (global, not a paint channel).
|
||||
var brush = NewSubPanel(root, PanelBrushSize, new Vector2(312f, -286f), w, "Brush");
|
||||
AddSliderStructure(brush, SliderBrushSize, "Brush Size", 1f, 256f, true);
|
||||
|
||||
return root.gameObject;
|
||||
}
|
||||
|
||||
/// <summary>Builds one self-contained, independently positioned channel card with a title bar.</summary>
|
||||
private RectTransform NewSubPanel(RectTransform root, string name, Vector2 pos, float width, string title)
|
||||
{
|
||||
var panel = NewRect(name, root);
|
||||
panel.anchorMin = panel.anchorMax = new Vector2(0f, 1f);
|
||||
panel.pivot = new Vector2(0f, 1f);
|
||||
panel.anchoredPosition = pos;
|
||||
panel.sizeDelta = new Vector2(width, 0f); // width fixed; height from the ContentSizeFitter
|
||||
AddImage(panel.gameObject, PanelColor);
|
||||
|
||||
var vlg = panel.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.padding = new RectOffset(10, 10, 10, 10);
|
||||
vlg.spacing = 8f;
|
||||
vlg.childAlignment = TextAnchor.UpperLeft;
|
||||
vlg.childControlWidth = true;
|
||||
vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
|
||||
var fitter = panel.gameObject.AddComponent<ContentSizeFitter>();
|
||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
AddPanelHeader(panel, title);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void AddPanelHeader(RectTransform panel, string title)
|
||||
{
|
||||
// Title bar. Its image is a raycast target so it can double as the drag handle in a later step.
|
||||
var rt = NewRect("Header", panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 24f;
|
||||
AddImage(rt.gameObject, HeaderColor);
|
||||
|
||||
var lbl = NewRect("Label", rt);
|
||||
lbl.anchorMin = Vector2.zero;
|
||||
lbl.anchorMax = Vector2.one;
|
||||
lbl.offsetMin = new Vector2(8f, 0f);
|
||||
lbl.offsetMax = new Vector2(-8f, 0f);
|
||||
var t = MakeText(lbl.gameObject, title, 13, FontStyle.Bold, new Color(0.9f, 0.9f, 0.95f, 1f), TextAnchor.MiddleLeft);
|
||||
t.raycastTarget = false;
|
||||
}
|
||||
|
||||
private void AddToggleStructure(RectTransform panel, string name, string label)
|
||||
{
|
||||
var rt = NewRect(name, panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 22f;
|
||||
|
||||
var toggle = rt.gameObject.AddComponent<Toggle>();
|
||||
|
||||
var box = NewRect("Box", rt);
|
||||
box.anchorMin = box.anchorMax = new Vector2(0f, 0.5f);
|
||||
box.pivot = new Vector2(0f, 0.5f);
|
||||
box.anchoredPosition = Vector2.zero;
|
||||
box.sizeDelta = new Vector2(18f, 18f);
|
||||
var boxImg = AddImage(box.gameObject, BoxColor);
|
||||
|
||||
var check = NewRect("Check", box);
|
||||
check.anchorMin = Vector2.zero;
|
||||
check.anchorMax = Vector2.one;
|
||||
check.offsetMin = new Vector2(3f, 3f);
|
||||
check.offsetMax = new Vector2(-3f, -3f);
|
||||
var checkImg = AddImage(check.gameObject, Accent);
|
||||
checkImg.raycastTarget = false;
|
||||
|
||||
var lbl = NewRect("Label", rt);
|
||||
lbl.anchorMin = Vector2.zero;
|
||||
lbl.anchorMax = Vector2.one;
|
||||
lbl.offsetMin = new Vector2(24f, 0f);
|
||||
lbl.offsetMax = Vector2.zero;
|
||||
var t = MakeText(lbl.gameObject, label, 14, FontStyle.Normal, Color.white, TextAnchor.MiddleLeft);
|
||||
t.raycastTarget = false;
|
||||
|
||||
toggle.targetGraphic = boxImg;
|
||||
toggle.graphic = checkImg;
|
||||
}
|
||||
|
||||
private void AddSliderStructure(RectTransform panel, string name, string label, float min, float max, bool whole)
|
||||
{
|
||||
var rt = NewRect(name, panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = 34f;
|
||||
|
||||
// Caption row (label left, value right), pinned to the top of the cell.
|
||||
var lbl = NewRect("Label", rt);
|
||||
lbl.anchorMin = new Vector2(0f, 1f);
|
||||
lbl.anchorMax = new Vector2(1f, 1f);
|
||||
lbl.pivot = new Vector2(0f, 1f);
|
||||
lbl.offsetMin = new Vector2(0f, -16f);
|
||||
lbl.offsetMax = new Vector2(-48f, 0f);
|
||||
var lt = MakeText(lbl.gameObject, label, 13, FontStyle.Normal, new Color(0.8f, 0.8f, 0.85f, 1f), TextAnchor.MiddleLeft);
|
||||
lt.raycastTarget = false;
|
||||
|
||||
var valRt = NewRect("Value", rt);
|
||||
valRt.anchorMin = new Vector2(1f, 1f);
|
||||
valRt.anchorMax = new Vector2(1f, 1f);
|
||||
valRt.pivot = new Vector2(1f, 1f);
|
||||
valRt.anchoredPosition = Vector2.zero;
|
||||
valRt.sizeDelta = new Vector2(56f, 16f);
|
||||
var vt = MakeText(valRt.gameObject, "", 13, FontStyle.Normal, Color.white, TextAnchor.MiddleRight);
|
||||
vt.raycastTarget = false;
|
||||
|
||||
// Track, pinned to the bottom of the cell.
|
||||
var track = NewRect("Track", rt);
|
||||
track.anchorMin = new Vector2(0f, 0f);
|
||||
track.anchorMax = new Vector2(1f, 0f);
|
||||
track.pivot = new Vector2(0.5f, 0f);
|
||||
track.anchoredPosition = Vector2.zero;
|
||||
track.sizeDelta = new Vector2(0f, 16f);
|
||||
var slider = track.gameObject.AddComponent<Slider>();
|
||||
|
||||
var bg = NewRect("Background", track);
|
||||
bg.anchorMin = new Vector2(0f, 0.5f);
|
||||
bg.anchorMax = new Vector2(1f, 0.5f);
|
||||
bg.pivot = new Vector2(0.5f, 0.5f);
|
||||
bg.anchoredPosition = Vector2.zero;
|
||||
bg.sizeDelta = new Vector2(0f, 5f);
|
||||
AddImage(bg.gameObject, BoxColor); // raycast target so clicking the bar jumps the value
|
||||
|
||||
var fillArea = NewRect("Fill Area", track);
|
||||
fillArea.anchorMin = new Vector2(0f, 0.5f);
|
||||
fillArea.anchorMax = new Vector2(1f, 0.5f);
|
||||
fillArea.pivot = new Vector2(0.5f, 0.5f);
|
||||
fillArea.anchoredPosition = Vector2.zero;
|
||||
fillArea.sizeDelta = new Vector2(-16f, 5f);
|
||||
var fill = NewRect("Fill", fillArea);
|
||||
fill.anchorMin = Vector2.zero;
|
||||
fill.anchorMax = Vector2.one;
|
||||
fill.offsetMin = Vector2.zero;
|
||||
fill.offsetMax = Vector2.zero;
|
||||
var fillImg = AddImage(fill.gameObject, Accent);
|
||||
fillImg.raycastTarget = false;
|
||||
|
||||
var handleArea = NewRect("Handle Slide Area", track);
|
||||
handleArea.anchorMin = Vector2.zero;
|
||||
handleArea.anchorMax = Vector2.one;
|
||||
handleArea.offsetMin = new Vector2(8f, 0f);
|
||||
handleArea.offsetMax = new Vector2(-8f, 0f);
|
||||
var handle = NewRect("Handle", handleArea);
|
||||
handle.sizeDelta = new Vector2(14f, 0f);
|
||||
var handleImg = AddImage(handle.gameObject, Color.white);
|
||||
|
||||
slider.fillRect = fill;
|
||||
slider.handleRect = handle;
|
||||
slider.targetGraphic = handleImg;
|
||||
slider.direction = Slider.Direction.LeftToRight;
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.wholeNumbers = whole; // serialized: the binder reads it back for value formatting
|
||||
}
|
||||
|
||||
private HsvColorPicker AddColorPickerStructure(RectTransform panel, string name)
|
||||
{
|
||||
const float square = 120f;
|
||||
const float hueH = 14f;
|
||||
|
||||
var rt = NewRect(name, panel);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.minHeight = le.preferredHeight = square + 6f + hueH;
|
||||
|
||||
var picker = rt.gameObject.AddComponent<HsvColorPicker>();
|
||||
picker.Build(square, hueH); // structure only; sprites + hooks are applied by Rebind in WirePanel
|
||||
return picker;
|
||||
}
|
||||
|
||||
// --- Wiring (behaviour; shared by the code-gen and prefab paths) ---
|
||||
|
||||
private void WirePanel(Transform panel)
|
||||
@@ -394,6 +173,23 @@ public class PaintHud : MonoBehaviour
|
||||
WireSlider(panel, SliderEmissionIntensity, painter ? painter.emissionIntensity : 1f, v => { if (painter) painter.emissionIntensity = v; });
|
||||
|
||||
WireSlider(panel, SliderBrushSize, painter ? painter.brushSize : 16f, v => { if (painter) painter.brushSize = Mathf.RoundToInt(v); });
|
||||
|
||||
foreach (var name in DraggablePanels)
|
||||
MakeDraggable(panel, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches (idempotently) a <see cref="DraggablePanel"/> to the whole panel so it can be dragged
|
||||
/// from any of its non-interactive surface (header, padding, labels, background); the component
|
||||
/// ignores presses that start on a widget (toggle, slider, colour picker).
|
||||
/// </summary>
|
||||
private static void MakeDraggable(Transform root, string panelName)
|
||||
{
|
||||
var panel = FindDeep(root, panelName) as RectTransform;
|
||||
if (!panel) return;
|
||||
var drag = panel.GetComponent<DraggablePanel>();
|
||||
if (!drag) drag = panel.gameObject.AddComponent<DraggablePanel>();
|
||||
drag.target = panel;
|
||||
}
|
||||
|
||||
private void EnsureFonts(Transform panel)
|
||||
@@ -417,7 +213,7 @@ public class PaintHud : MonoBehaviour
|
||||
var slider = cell.GetComponentInChildren<Slider>();
|
||||
if (!slider) return;
|
||||
var valueRt = cell.Find("Value");
|
||||
var valueText = valueRt ? valueRt.GetComponent<Text>() : null;
|
||||
var valueText = valueRt ? valueRt.GetComponent<TMPro.TMP_Text>() : null;
|
||||
|
||||
bool whole = slider.wholeNumbers;
|
||||
slider.value = value;
|
||||
@@ -457,20 +253,6 @@ public class PaintHud : MonoBehaviour
|
||||
return null;
|
||||
}
|
||||
|
||||
private Text MakeText(GameObject go, string text, int size, FontStyle style, Color color, TextAnchor anchor)
|
||||
{
|
||||
var t = go.AddComponent<Text>();
|
||||
t.font = _font;
|
||||
t.fontSize = size;
|
||||
t.fontStyle = style;
|
||||
t.color = color;
|
||||
t.alignment = anchor;
|
||||
t.text = text;
|
||||
t.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
t.verticalOverflow = VerticalWrapMode.Truncate;
|
||||
return t;
|
||||
}
|
||||
|
||||
// --- Static UI factory (shared with HsvColorPicker) ---
|
||||
|
||||
public static RectTransform NewRect(string name, Transform parent)
|
||||
|
||||
Reference in new issue
Block a user