using Nova; using System.Collections.Generic; using UnityEngine; namespace NovaSamples.UIControls { /// /// A simple positionable and resizable window component /// /// /// [RequireComponent(typeof(UIBlock))] public class Window : MonoBehaviour { /// /// The attached to this game object. /// private UIBlock uiBlock = null; /// /// The attached to this game object. /// public UIBlock UIBlock { get { if (uiBlock == null) { uiBlock = GetComponent(); } return uiBlock; } } /// /// Cache the bounds of the window when the resize gesture begins /// private Dictionary cachedBounds = new Dictionary(); private void OnEnable() { // Subscribe to gestures UIBlock.AddGestureHandler(HandleWindowDragged); UIBlock.AddGestureHandler(HandleResizeRegionDragged); UIBlock.AddGestureHandler(HandleResizeRegionHovered); UIBlock.AddGestureHandler(HandleResizeRegionUnhovered); UIBlock.AddGestureHandler(HandleResizeRegionReleased); UIBlock.AddGestureHandler(HandleResizeRegionGestureCanceled); // Ensure known starting state cachedBounds.Clear(); } private void OnDisable() { // Unsubscribe from gestures UIBlock.RemoveGestureHandler(HandleWindowDragged); UIBlock.RemoveGestureHandler(HandleResizeRegionDragged); UIBlock.RemoveGestureHandler(HandleResizeRegionHovered); UIBlock.RemoveGestureHandler(HandleResizeRegionUnhovered); UIBlock.RemoveGestureHandler(HandleResizeRegionReleased); UIBlock.RemoveGestureHandler(HandleResizeRegionGestureCanceled); // Restore the default cursor SetCursor(null); } /// /// Move this window by the amount it was dragged /// private void HandleWindowDragged(Gesture.OnDrag evt, WindowBar windowBar) { Vector3 positionDelta = evt.DragDeltaLocalSpace; Transform parent = UIBlock.transform.parent; if (parent != null) { // Transform drag delta into parent space Matrix4x4 localToParent = Matrix4x4.TRS(UIBlock.transform.localPosition, UIBlock.transform.localRotation, UIBlock.transform.localScale); positionDelta = localToParent.MultiplyVector(positionDelta); } UIBlock.TrySetLocalPosition(UIBlock.transform.localPosition + positionDelta); } /// /// Resize the window from the draggable region /// private void HandleResizeRegionDragged(Gesture.OnDrag evt, WindowResizeRegion region) { // Determine which region was dragged based on its local position relative to the center of this window Vector3 regionLocalPosition = UIBlock.transform.InverseTransformPoint(region.View.transform.position); Vector3 regionVector = Sign(regionLocalPosition); // Get the total translation along the region's draggable axes Vector3 dragAxes = new Vector3(evt.DraggableAxes.X ? 1 : 0, evt.DraggableAxes.Y ? 1 : 0, evt.DraggableAxes.Z ? 1 : 0); Vector3 dragTranslation = Vector3.Scale(evt.RawTranslationLocalSpace, dragAxes); Vector3 sizeDelta = Vector3.Scale(dragTranslation, regionVector); if (!cachedBounds.TryGetValue(evt.Interaction.ControlID, out Bounds startingBounds)) { // Cache the initial bounds once the gesture starts, // so we can track total size and position change startingBounds = new Bounds(UIBlock.transform.localPosition, UIBlock.CalculatedSize.Value); cachedBounds.Add(evt.Interaction.ControlID, startingBounds); } // Adjust the size of the window UIBlock.Size = startingBounds.size + sizeDelta; // Adjust the window's position such that the opposite side of the drag region remains in the same position Vector3 rawPositionDelta = dragTranslation * 0.5f; Vector3 positionDelta = ClampPositionDelta(rawPositionDelta, sizeDelta, startingBounds.size, UIBlock.SizeMinMax); Transform parent = UIBlock.transform.parent; if (parent != null) { // Transform drag delta into parent space Matrix4x4 localToParent = Matrix4x4.TRS(startingBounds.center, UIBlock.transform.localRotation, UIBlock.transform.localScale); positionDelta = localToParent.MultiplyVector(positionDelta); } UIBlock.TrySetLocalPosition(startingBounds.center + positionDelta); } /// /// Change the cursor to indicate how the window will resize if it's clicked and draggged /// private void HandleResizeRegionHovered(Gesture.OnHover evt, WindowResizeRegion region) { SetCursor(region.Cursor); } /// /// Restore the cursor when a resize region is unhovered /// private void HandleResizeRegionUnhovered(Gesture.OnUnhover evt, WindowResizeRegion region) { SetCursor(null); } /// /// Clear the cached size for the released pointer /// private void HandleResizeRegionReleased(Gesture.OnRelease evt, WindowResizeRegion region) => cachedBounds.Remove(evt.Interaction.ControlID); /// /// Clear the cached size for the canceled pointer /// private void HandleResizeRegionGestureCanceled(Gesture.OnCancel evt, WindowResizeRegion region) => cachedBounds.Remove(evt.Interaction.ControlID); private static Vector3 Sign(Vector3 v) => new Vector3(Sign(v.x), Sign(v.y), Sign(v.z)); /// /// Unity's method returns 1 when the provided float is 0. /// /// The float to check the sign of. /// The sign of . private static float Sign(float f) => f < 0 ? -1 : f > 0 ? 1 : 0; /// /// Clamp the such that the position only changes as much as the UIBlock is resized. /// /// /// Handles the case where if the UIBlock has a minimum (or maximum) size, we don't want to continue moving the UIBlock once that minimum (or maximum) size is reached while the user is making the window smaller (or larger). /// private static Vector3 ClampPositionDelta(Vector3 positionDelta, Vector3 sizeDelta, Vector3 currentSize, MinMax3 sizeRange) { return new Vector3(ClampPositionDelta(positionDelta.x, sizeDelta.x, currentSize.x, sizeRange.X), ClampPositionDelta(positionDelta.y, sizeDelta.y, currentSize.y, sizeRange.Y), ClampPositionDelta(positionDelta.z, sizeDelta.z, currentSize.z, sizeRange.Z)); } /// /// Clamp the such that the position only changes as much as the UIBlock is resized. /// /// /// Handles the case where if the UIBlock has a minimum (or maximum) size, we don't want to continue moving the UIBlock once that minimum (or maximum) size is reached while the user is making the window smaller (or larger). /// private static float ClampPositionDelta(float rawPositionDelta, float sizeDelta, float currentSize, MinMax range) { float clampedSizeDelta = range.Clamp(sizeDelta + currentSize) - currentSize; float clampedPositionDeltaRange = clampedSizeDelta * 0.5f; float min = Mathf.Min(clampedPositionDeltaRange, -clampedPositionDeltaRange); float max = Mathf.Max(clampedPositionDeltaRange, -clampedPositionDeltaRange); return Mathf.Clamp(rawPositionDelta, min, max); } /// /// Assign the cursor to a custom texture, or set it to null to restore the default cursor. /// /// The texture to assign to the cursor private static void SetCursor(Texture2D cursorTexture) { // offset is 0 if the texture is null, otherwise we want to center the cursor texture Vector2 offset = cursorTexture == null ? Vector2.zero : new Vector2(cursorTexture.width, cursorTexture.height) * 0.5f; Cursor.SetCursor(cursorTexture, offset, CursorMode.Auto); } } }