using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; /// /// Makes a UI panel draggable. Attach to the panel itself: it follows the pointer from any of the panel's /// non-interactive surface (title bar, padding, labels, background) but ignores presses that begin on a /// widget - a (toggle, slider, ...) or a (the /// colour-picker field) - so those keep their own click/drag behaviour. It keeps a slice of the panel on /// screen and raises it above its siblings while interacting. /// /// Works under any canvas scale (it tracks the pointer in the drag area's local space and applies the /// movement delta) and under any anchor/pivot (the on-screen clamp is derived from the panel's real /// corners in its parent, not from a fixed top-left assumption). /// [RequireComponent(typeof(RectTransform))] public class DraggablePanel : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler { [Tooltip("Panel moved by the drag. Empty = this object's RectTransform.")] public RectTransform target; [Tooltip("Panel pixels (canvas units) kept on screen on every edge so it can never be lost.")] public float keepVisible = 48f; private RectTransform _area; // the panel's parent; pointer positions are tracked in its local space private Vector2 _grabOffset; private bool _dragging; private void Awake() => Resolve(); private void Resolve() { if (!target) target = transform as RectTransform; if (target) _area = target.parent as RectTransform; } public void OnPointerDown(PointerEventData eventData) { // Raise the panel when pressing its own surface. (A press that lands on a widget is consumed by // that widget's own handler and never reaches us, so this rarely needs the guard - but it is cheap.) if (!StartedOnWidget(eventData)) BringToFront(); } public void OnBeginDrag(PointerEventData eventData) { _dragging = false; if (!target) Resolve(); if (!target || !_area) return; if (StartedOnWidget(eventData)) return; // a toggle/slider/picker owns this gesture, not the panel _dragging = true; BringToFront(); // Capture grab offset; the constant frame difference between the area pivot and the panel anchor // cancels out because we only ever apply the pointer delta on top of this offset. if (RectTransformUtility.ScreenPointToLocalPointInRectangle( _area, eventData.position, eventData.pressEventCamera, out var local)) _grabOffset = target.anchoredPosition - local; } public void OnDrag(PointerEventData eventData) { if (!_dragging || !target || !_area) return; if (!RectTransformUtility.ScreenPointToLocalPointInRectangle( _area, eventData.position, eventData.pressEventCamera, out var local)) return; target.anchoredPosition = Clamp(local + _grabOffset); } // True when the press started on an interactive widget inside the panel (which then owns the gesture). private bool StartedOnWidget(PointerEventData eventData) { var hit = eventData.pointerPressRaycast.gameObject; for (var t = hit ? hit.transform : null; t && t != target; t = t.parent) if (t.GetComponent() || t.GetComponent()) return true; return false; } private void BringToFront() { if (target) target.SetAsLastSibling(); } // Anchor/pivot-agnostic clamp: keep at least `keepVisible` px of the panel inside the parent on every // edge, computed from the panel's real corners so it is correct wherever the panel is anchored. private Vector2 Clamp(Vector2 pos) { if (!_area) return pos; Rect area = _area.rect; Rect size = target.rect; Vector2 pivot = target.pivot; // Anchor reference point in the area's local space (panels are point-anchored: anchorMin == anchorMax). Vector2 anchorRef = new Vector2( area.xMin + target.anchorMin.x * area.width, area.yMin + target.anchorMin.y * area.height); Vector2 pivotToCorner = new Vector2(pivot.x * size.width, pivot.y * size.height); // Bottom-left corner of the panel for this anchoredPosition. Vector2 corner = anchorRef + pos - pivotToCorner; float keepX = Mathf.Min(keepVisible, size.width); float keepY = Mathf.Min(keepVisible, size.height); corner.x = Mathf.Clamp(corner.x, area.xMin + keepX - size.width, area.xMax - keepX); corner.y = Mathf.Clamp(corner.y, area.yMin + keepY - size.height, area.yMax - keepY); return corner + pivotToCorner - anchorRef; } }