diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index 7237d724b..0a5c520a7 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -117,5 +117,10 @@ namespace FlaxEditor.Gizmo /// /// The new actor to spawn. void Spawn(Actor actor); + + /// + /// Opens the context menu at the current mouse location (using current selection). + /// + void OpenContextMenu(); } } diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 64f969990..cd5e16e92 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -42,6 +42,11 @@ namespace FlaxEditor.Gizmo /// public Action Duplicate; + /// + /// Gets the array of selected objects. + /// + public List Selection => _selection; + /// /// Gets the array of selected parent objects (as actors). /// diff --git a/Source/Editor/Gizmo/UIEditorGizmo.cs b/Source/Editor/Gizmo/UIEditorGizmo.cs new file mode 100644 index 000000000..4fd059202 --- /dev/null +++ b/Source/Editor/Gizmo/UIEditorGizmo.cs @@ -0,0 +1,724 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using FlaxEditor.Gizmo; +using FlaxEditor.SceneGraph; +using FlaxEditor.SceneGraph.Actors; +using FlaxEditor.Viewport.Cameras; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor +{ + /// + /// UI editor camera. + /// + [HideInEditor] + internal sealed class UIEditorCamera : ViewportCamera + { + public UIEditorRoot UIEditor; + + public void ShowActors(IEnumerable actors) + { + // Calculate bounds of all selected objects + var areaRect = Rectangle.Empty; + var root = UIEditor.UIRoot; + foreach (var actor in actors) + { + Rectangle bounds; + if (actor is UIControl uiControl && uiControl.HasControl && uiControl.IsActive) + { + var control = uiControl.Control; + bounds = control.EditorBounds; + + var ul = control.PointToParent(root, bounds.UpperLeft); + var ur = control.PointToParent(root, bounds.UpperRight); + var bl = control.PointToParent(root, bounds.BottomLeft); + var br = control.PointToParent(root, bounds.BottomRight); + + var min = Float2.Min(Float2.Min(ul, ur), Float2.Min(bl, br)); + var max = Float2.Max(Float2.Max(ul, ur), Float2.Max(bl, br)); + bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero)); + } + else if (actor is UICanvas uiCanvas && uiCanvas.IsActive && uiCanvas.GUI.Parent == root) + { + bounds = uiCanvas.GUI.Bounds; + } + else + continue; + + if (areaRect == Rectangle.Empty) + areaRect = bounds; + else + areaRect = Rectangle.Union(areaRect, bounds); + } + if (areaRect == Rectangle.Empty) + return; + + // Add margin + areaRect = areaRect.MakeExpanded(100.0f); + + // Show bounds + UIEditor.ViewScale = (UIEditor.Size / areaRect.Size).MinValue * 0.95f; + UIEditor.ViewCenterPosition = areaRect.Center; + } + + public override void FocusSelection(GizmosCollection gizmos, ref Quaternion orientation) + { + ShowActors(gizmos.Get().Selection, ref orientation); + } + + public override void ShowActor(Actor actor) + { + ShowActors(new[] { actor }); + } + + public override void ShowActors(List selection, ref Quaternion orientation) + { + ShowActors(selection.ConvertAll(x => (Actor)x.EditableObject)); + } + + public override void UpdateView(float dt, ref Vector3 moveDelta, ref Float2 mouseDelta, out bool centerMouse) + { + centerMouse = false; + } + } + + /// + /// Root control for UI Controls presentation in the game/prefab viewport. + /// + [HideInEditor] + internal class UIEditorRoot : InputsPassThrough + { + /// + /// View for the UI structure to be linked in for camera zoom and panning operations. + /// + private sealed class View : ContainerControl + { + public View(UIEditorRoot parent) + { + AutoFocus = false; + ClipChildren = false; + CullChildren = false; + Pivot = Float2.Zero; + Size = new Float2(1920, 1080); + Parent = parent; + } + + public override bool RayCast(ref Float2 location, out Control hit) + { + // Ignore self + return RayCastChildren(ref location, out hit); + } + + public override bool IntersectsContent(ref Float2 locationParent, out Float2 location) + { + location = PointFromParent(ref locationParent); + return true; + } + + public override void DrawSelf() + { + var uiRoot = (UIEditorRoot)Parent; + if (!uiRoot.EnableBackground) + return; + + // Draw canvas area + var bounds = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(bounds, new Color(0, 0, 0, 0.2f)); + } + } + + private bool _mouseMovesControl, _mouseMovesView; + private Float2 _mouseMovesPos, _moveSnapDelta; + private float _mouseMoveSum; + private UndoMultiBlock _undoBlock; + private View _view; + private float[] _gridTickSteps = Utilities.Utils.CurveTickSteps, _gridTickStrengths; + + /// + /// True if enable displaying UI editing background and grid elements. + /// + public virtual bool EnableBackground => false; + + /// + /// True if enable selecting controls with mouse button. + /// + public virtual bool EnableSelecting => false; + + /// + /// True if enable panning and zooming the view. + /// + public bool EnableCamera => _view != null; + + /// + /// Transform gizmo to use sync with (selection, snapping, transformation settings). + /// + public virtual TransformGizmo TransformGizmo => null; + + /// + /// The root control for controls to be linked in. + /// + public readonly ContainerControl UIRoot; + + internal Float2 ViewPosition + { + get => _view.Location / -ViewScale; + set => _view.Location = value * -ViewScale; + } + + internal Float2 ViewCenterPosition + { + get => (_view.Location - Size * 0.5f) / -ViewScale; + set => _view.Location = Size * 0.5f + value * -ViewScale; + } + + internal float ViewScale + { + get => _view.Scale.X; + set + { + value = Mathf.Clamp(value, 0.1f, 4.0f); + _view.Scale = new Float2(value); + } + } + + public UIEditorRoot(bool enableCamera = false) + { + AnchorPreset = AnchorPresets.StretchAll; + Offsets = Margin.Zero; + AutoFocus = false; + UIRoot = this; + CullChildren = false; + ClipChildren = true; + if (enableCamera) + { + _view = new View(this); + UIRoot = _view; + } + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + return true; + + var transformGizmo = TransformGizmo; + var owner = transformGizmo?.Owner; + if (EnableSelecting && owner != null && !_mouseMovesControl && button == MouseButton.Left) + { + // Raycast the control under the mouse + var mousePos = PointFromWindow(RootWindow.MousePosition); + if (RayCastControl(ref mousePos, out var hitControl)) + { + var uiControlNode = FindUIControlNode(hitControl); + if (uiControlNode != null) + { + // Select node (with additive mode) + var selection = new List(); + if (Root.GetKey(KeyboardKeys.Control)) + { + // Add/remove from selection + selection.AddRange(transformGizmo.Selection); + if (transformGizmo.Selection.Contains(uiControlNode)) + selection.Remove(uiControlNode); + else + selection.Add(uiControlNode); + } + else + { + // Select + selection.Add(uiControlNode); + } + owner.Select(selection); + + // Initialize control movement + _mouseMovesControl = true; + _mouseMovesPos = location; + _mouseMoveSum = 0.0f; + _moveSnapDelta = Float2.Zero; + Focus(); + StartMouseCapture(); + return true; + } + } + } + if (EnableCamera && (button == MouseButton.Right || button == MouseButton.Middle)) + { + // Initialize surface movement + _mouseMovesView = true; + _mouseMovesPos = location; + _mouseMoveSum = 0.0f; + Focus(); + StartMouseCapture(); + return true; + } + + return false; + } + + public override void OnMouseMove(Float2 location) + { + base.OnMouseMove(location); + + var transformGizmo = TransformGizmo; + if (_mouseMovesControl && transformGizmo != null) + { + // Calculate transform delta + var delta = location - _mouseMovesPos; + if (transformGizmo.TranslationSnapEnable || transformGizmo.Owner.UseSnapping) + { + _moveSnapDelta += delta; + delta = Float2.SnapToGrid(_moveSnapDelta, new Float2(transformGizmo.TranslationSnapValue * ViewScale)); + _moveSnapDelta -= delta; + } + + // Move selected controls + if (delta.LengthSquared > 0.0f) + { + StartUndo(); + var moved = false; + var moveLocation = _mouseMovesPos + delta; + var selection = transformGizmo.Selection; + for (var i = 0; i < selection.Count; i++) + { + if (IsValidControl(selection[i], out var uiControl)) + { + // Move control (handle any control transformations by moving in editor's local-space) + var control = uiControl.Control; + var localLocation = control.LocalLocation; + var pointOrigin = control.Parent ?? control; + var startPos = pointOrigin.PointFromParent(this, _mouseMovesPos); + var endPos = pointOrigin.PointFromParent(this, moveLocation); + var uiControlDelta = endPos - startPos; + control.LocalLocation = localLocation + uiControlDelta; + + // Don't move if layout doesn't allow it + if (control.Parent != null) + control.Parent.PerformLayout(); + else + control.PerformLayout(); + + // Check if control was moved (parent container could block it) + if (localLocation != control.LocalLocation) + moved = true; + } + } + _mouseMovesPos = location; + _mouseMoveSum += delta.Length; + if (moved) + Cursor = CursorType.SizeAll; + } + } + if (_mouseMovesView) + { + // Move view + var delta = location - _mouseMovesPos; + if (delta.LengthSquared > 4.0f) + { + _mouseMovesPos = location; + _mouseMoveSum += delta.Length; + _view.Location += delta; + Cursor = CursorType.SizeAll; + } + } + } + + public override bool OnMouseUp(Float2 location, MouseButton button) + { + EndMovingControls(); + if (_mouseMovesView) + { + EndMovingView(); + if (button == MouseButton.Right && _mouseMoveSum < 2.0f) + TransformGizmo.Owner.OpenContextMenu(); + } + + return base.OnMouseUp(location, button); + } + + public override void OnMouseLeave() + { + EndMovingControls(); + EndMovingView(); + + base.OnMouseLeave(); + } + + public override void OnLostFocus() + { + EndMovingControls(); + EndMovingView(); + + base.OnLostFocus(); + } + + public override bool OnMouseWheel(Float2 location, float delta) + { + if (base.OnMouseWheel(location, delta)) + return true; + + if (EnableCamera && !_mouseMovesControl) + { + // Zoom view + var nextViewScale = ViewScale + delta * 0.1f; + if (delta > 0 && !_mouseMovesControl) + { + // Scale towards mouse when zooming in + var nextCenterPosition = ViewPosition + location / ViewScale; + ViewScale = nextViewScale; + ViewPosition = nextCenterPosition - (location / ViewScale); + } + else + { + // Scale while keeping center position when zooming out or when dragging view + var viewCenter = ViewCenterPosition; + ViewScale = nextViewScale; + ViewCenterPosition = viewCenter; + } + + return true; + } + + return false; + } + + public override void Draw() + { + if (EnableBackground) + { + // Draw background + Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Width, Height); + + // Draw grid + var viewRect = GetClientArea(); + var upperLeft = _view.PointFromParent(viewRect.Location); + var bottomRight = _view.PointFromParent(viewRect.Size); + var min = Float2.Min(upperLeft, bottomRight); + var max = Float2.Max(upperLeft, bottomRight); + var pixelRange = (max - min) * ViewScale; + Render2D.PushClip(ref viewRect); + DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X); + DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y); + Render2D.PopClip(); + } + + base.Draw(); + + bool drawAnySelectedControl = false; + var transformGizmo = TransformGizmo; + if (transformGizmo != null) + { + // Selected UI controls outline + var selection = transformGizmo.Selection; + for (var i = 0; i < selection.Count; i++) + { + if (IsValidControl(selection[i], out var controlActor)) + { + DrawControlBounds(controlActor.Control, true, ref drawAnySelectedControl); + // TODO: draw anchors + } + } + } + if (EnableSelecting && !_mouseMovesControl && IsMouseOver) + { + // Highlight control under mouse for easier selecting (except if already selected) + var mousePos = PointFromWindow(RootWindow.MousePosition); + if (RayCastControl(ref mousePos, out var hitControl) && + (transformGizmo == null || !transformGizmo.Selection.Any(x => x.EditableObject is UIControl controlActor && controlActor.Control == hitControl))) + { + DrawControlBounds(hitControl, false, ref drawAnySelectedControl); + } + } + if (drawAnySelectedControl) + Render2D.PopTransform(); + + if (EnableBackground) + { + // Draw border + if (ContainsFocus) + { + Render2D.DrawRectangle(new Rectangle(1, 1, Width - 2, Height - 2), Editor.IsPlayMode ? Color.OrangeRed : Style.Current.BackgroundSelected); + } + } + } + + public override void OnDestroy() + { + if (IsDisposing) + return; + EndMovingControls(); + EndMovingView(); + + base.OnDestroy(); + } + + private void DrawAxis(Float2 axis, Rectangle viewRect, float min, float max, float pixelRange) + { + var style = Style.Current; + var linesColor = style.ForegroundDisabled.RGBMultiplied(0.5f); + var labelsColor = style.ForegroundDisabled; + var labelsSize = 10.0f; + Utilities.Utils.DrawCurveTicks((float tick, float strength) => + { + var p = _view.PointToParent(axis * tick); + + // Draw line + var lineRect = new Rectangle + ( + viewRect.Location + (p - 0.5f) * axis, + Float2.Lerp(viewRect.Size, Float2.One, axis) + ); + Render2D.FillRectangle(lineRect, linesColor.AlphaMultiplied(strength)); + + // Draw label + string label = tick.ToString(System.Globalization.CultureInfo.InvariantCulture); + var labelRect = new Rectangle + ( + viewRect.X + 4.0f + (p.X * axis.X), + viewRect.Y - labelsSize + (p.Y * axis.Y) + (viewRect.Size.Y * axis.X), + 50, + labelsSize + ); + Render2D.DrawText(style.FontSmall, label, labelRect, labelsColor.AlphaMultiplied(strength), TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.7f); + }, _gridTickSteps, ref _gridTickStrengths, min, max, pixelRange); + } + + private void DrawControlBounds(Control control, bool selection, ref bool drawAnySelectedControl) + { + if (!drawAnySelectedControl) + { + drawAnySelectedControl = true; + Render2D.PushTransform(ref _cachedTransform); + } + var options = Editor.Instance.Options.Options.Visual; + var bounds = control.EditorBounds; + var ul = control.PointToParent(this, bounds.UpperLeft); + var ur = control.PointToParent(this, bounds.UpperRight); + var bl = control.PointToParent(this, bounds.BottomLeft); + var br = control.PointToParent(this, bounds.BottomRight); + var color = selection ? options.SelectionOutlineColor0 : Style.Current.SelectionBorder; +#if false + // AABB + var min = Float2.Min(Float2.Min(ul, ur), Float2.Min(bl, br)); + var max = Float2.Max(Float2.Max(ul, ur), Float2.Max(bl, br)); + bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero)); + Render2D.DrawRectangle(bounds, color, options.UISelectionOutlineSize); +#else + // OBB + Render2D.DrawLine(ul, ur, color, options.UISelectionOutlineSize); + Render2D.DrawLine(ur, br, color, options.UISelectionOutlineSize); + Render2D.DrawLine(br, bl, color, options.UISelectionOutlineSize); + Render2D.DrawLine(bl, ul, color, options.UISelectionOutlineSize); +#endif + } + + private bool IsValidControl(SceneGraphNode node, out UIControl uiControl) + { + uiControl = null; + if (node.EditableObject is UIControl controlActor) + uiControl = controlActor; + return uiControl != null && + uiControl.Control != null && + uiControl.Control.VisibleInHierarchy && + uiControl.Control.RootWindow != null; + } + + private bool RayCastControl(ref Float2 location, out Control hit) + { +#if false + // Raycast only controls with content (eg. skips transparent panels) + return RayCastChildren(ref location, out hit); +#else + // Find any control under mouse (hierarchical) + hit = GetChildAtRecursive(location); + if (hit is View) + hit = null; + return hit != null; +#endif + } + + private UIControlNode FindUIControlNode(Control control) + { + return FindUIControlNode(TransformGizmo.Owner.SceneGraphRoot, control); + } + + private UIControlNode FindUIControlNode(SceneGraphNode node, Control control) + { + var result = node as UIControlNode; + if (result != null && ((UIControl)result.Actor).Control == control) + return result; + foreach (var e in node.ChildNodes) + { + result = FindUIControlNode(e, control); + if (result != null) + return result; + } + return null; + } + + private void StartUndo() + { + var undo = TransformGizmo?.Owner?.Undo; + if (undo == null || _undoBlock != null) + return; + _undoBlock = new UndoMultiBlock(undo, TransformGizmo.Selection.ConvertAll(x => x.EditableObject), "Edit control"); + } + + private void EndUndo() + { + if (_undoBlock == null) + return; + _undoBlock.Dispose(); + _undoBlock = null; + } + + private void EndMovingControls() + { + if (!_mouseMovesControl) + return; + _mouseMovesControl = false; + EndMouseCapture(); + Cursor = CursorType.Default; + EndUndo(); + } + + private void EndMovingView() + { + if (!_mouseMovesView) + return; + _mouseMovesView = false; + EndMouseCapture(); + Cursor = CursorType.Default; + } + } + + /// + /// Control that can optionally disable inputs to the children. + /// + [HideInEditor] + internal class InputsPassThrough : ContainerControl + { + private bool _isMouseOver; + + /// + /// True if enable input events passing to the UI. + /// + public virtual bool EnableInputs => true; + + public override bool RayCast(ref Float2 location, out Control hit) + { + return RayCastChildren(ref location, out hit); + } + + public override bool ContainsPoint(ref Float2 location, bool precise = false) + { + if (precise) + return false; + return base.ContainsPoint(ref location, precise); + } + + public override bool OnCharInput(char c) + { + if (!EnableInputs) + return false; + return base.OnCharInput(c); + } + + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + if (!EnableInputs) + return DragDropEffect.None; + return base.OnDragDrop(ref location, data); + } + + public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) + { + if (!EnableInputs) + return DragDropEffect.None; + return base.OnDragEnter(ref location, data); + } + + public override void OnDragLeave() + { + if (!EnableInputs) + return; + base.OnDragLeave(); + } + + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + if (!EnableInputs) + return DragDropEffect.None; + return base.OnDragMove(ref location, data); + } + + public override bool OnKeyDown(KeyboardKeys key) + { + if (!EnableInputs) + return false; + return base.OnKeyDown(key); + } + + public override void OnKeyUp(KeyboardKeys key) + { + if (!EnableInputs) + return; + base.OnKeyUp(key); + } + + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (!EnableInputs) + return false; + return base.OnMouseDoubleClick(location, button); + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (!EnableInputs) + return false; + return base.OnMouseDown(location, button); + } + + public override bool IsMouseOver => _isMouseOver; + + public override void OnMouseEnter(Float2 location) + { + _isMouseOver = true; + if (!EnableInputs) + return; + base.OnMouseEnter(location); + } + + public override void OnMouseLeave() + { + _isMouseOver = false; + if (!EnableInputs) + return; + base.OnMouseLeave(); + } + + public override void OnMouseMove(Float2 location) + { + if (!EnableInputs) + return; + base.OnMouseMove(location); + } + + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (!EnableInputs) + return false; + return base.OnMouseUp(location, button); + } + + public override bool OnMouseWheel(Float2 location, float delta) + { + if (!EnableInputs) + return false; + return base.OnMouseWheel(location, delta); + } + } +} diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index 13da1fb97..59a56e3f3 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -64,7 +64,11 @@ namespace FlaxEditor.Surface /// protected virtual void DrawBackground() { - var background = Style.Background; + DrawBackgroundDefault(Style.Background, Width, Height); + } + + internal static void DrawBackgroundDefault(Texture background, float width, float height) + { if (background && background.ResidentMipLevels > 0) { var bSize = background.Size; @@ -77,8 +81,8 @@ namespace FlaxEditor.Surface if (pos.Y > 0) pos.Y -= bh; - int maxI = Mathf.CeilToInt(Width / bw + 1.0f); - int maxJ = Mathf.CeilToInt(Height / bh + 1.0f); + int maxI = Mathf.CeilToInt(width / bw + 1.0f); + int maxJ = Mathf.CeilToInt(height / bh + 1.0f); for (int i = 0; i < maxI; i++) { diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index f1c4fed70..fec1154a5 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -93,6 +93,9 @@ namespace FlaxEditor.Viewport /// public abstract void Spawn(Actor actor); + /// + public abstract void OpenContextMenu(); + /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 785f0e651..59fad6b11 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -10,7 +10,6 @@ using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Widgets; using FlaxEngine; using FlaxEngine.GUI; -using Newtonsoft.Json; using JsonSerializer = FlaxEngine.Json.JsonSerializer; namespace FlaxEditor.Viewport @@ -154,6 +153,7 @@ namespace FlaxEditor.Viewport // Input + internal bool _disableInputUpdate; private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown; private int _deltaFilteringStep; private Float2 _startPos; @@ -1496,6 +1496,9 @@ namespace FlaxEditor.Viewport { base.Update(deltaTime); + if (_disableInputUpdate) + return; + // Update camera bool useMovementSpeed = false; if (_camera != null) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 6d8b572a4..c9bb4c88a 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; @@ -223,7 +222,7 @@ namespace FlaxEditor.Viewport TransformGizmo = new TransformGizmo(this); TransformGizmo.ApplyTransformation += ApplyTransform; TransformGizmo.ModeChanged += OnGizmoModeChanged; - TransformGizmo.Duplicate += Editor.Instance.SceneEditing.Duplicate; + TransformGizmo.Duplicate += _editor.SceneEditing.Duplicate; Gizmos.Active = TransformGizmo; // Add grid @@ -479,7 +478,7 @@ namespace FlaxEditor.Viewport }; // Spawn - Editor.Instance.SceneEditing.Spawn(actor, parent); + _editor.SceneEditing.Spawn(actor, parent); } private void OnBegin(RenderTask task, GPUContext context) @@ -712,7 +711,7 @@ namespace FlaxEditor.Viewport Vector3 gizmoPosition = TransformGizmo.Position; // Rotate selected objects - bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode; + bool isPlayMode = _editor.StateMachine.IsPlayMode; TransformGizmo.StartTransforming(); for (int i = 0; i < selection.Count; i++) { @@ -787,7 +786,7 @@ namespace FlaxEditor.Viewport Vector3 gizmoPosition = TransformGizmo.Position; // Transform selected objects - bool isPlayMode = Editor.Instance.StateMachine.IsPlayMode; + bool isPlayMode = _editor.StateMachine.IsPlayMode; for (int i = 0; i < selection.Count; i++) { var obj = selection[i]; @@ -929,7 +928,14 @@ namespace FlaxEditor.Viewport { var parent = actor.Parent ?? Level.GetScene(0); actor.Name = Utilities.Utils.IncrementNameNumber(actor.Name, x => parent.GetChild(x) == null); - Editor.Instance.SceneEditing.Spawn(actor); + _editor.SceneEditing.Spawn(actor); + } + + /// + public override void OpenContextMenu() + { + var mouse = PointFromWindow(Root.MousePosition); + _editor.Windows.SceneWin.ShowContextMenu(this, mouse); } /// diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 4166d6163..31f27f708 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -29,7 +29,6 @@ namespace FlaxEditor.Viewport { public PrefabWindowViewport Viewport; - /// public override bool CanRender() { return (Task.View.Flags & ViewFlags.EditorSprites) == ViewFlags.EditorSprites && Enabled; @@ -41,6 +40,24 @@ namespace FlaxEditor.Viewport } } + [HideInEditor] + private sealed class PrefabUIEditorRoot : UIEditorRoot + { + private readonly PrefabWindowViewport _viewport; + + public PrefabUIEditorRoot(PrefabWindowViewport viewport) + : base(true) + { + _viewport = viewport; + Parent = viewport; + } + + public override bool EnableInputs => false; + public override bool EnableSelecting => true; + public override bool EnableBackground => _viewport._hasUILinkedCached; + public override TransformGizmo TransformGizmo => _viewport.TransformGizmo; + } + private readonly PrefabWindow _window; private UpdateDelegate _update; @@ -56,6 +73,9 @@ namespace FlaxEditor.Viewport private PrefabSpritesRenderer _spritesRenderer; private IntPtr _tempDebugDrawContext; + private bool _hasUILinkedCached; + private PrefabUIEditorRoot _uiRoot; + /// /// Drag and drop handlers /// @@ -111,6 +131,11 @@ namespace FlaxEditor.Viewport TransformGizmo.Duplicate += _window.Duplicate; Gizmos.Active = TransformGizmo; + // Use custom root for UI controls + _uiRoot = new PrefabUIEditorRoot(this); + _uiRoot.IndexInParent = 0; // Move viewport down below other widgets in the viewport + _uiParentLink = _uiRoot.UIRoot; + // Transform space widget var transformSpaceWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); var transformSpaceToggle = new ViewportWidgetButton(string.Empty, window.Editor.Icons.Globe32, null, true) @@ -237,8 +262,54 @@ namespace FlaxEditor.Viewport SetUpdate(ref _update, OnUpdate); } + /// + /// Updates the viewport's gizmos, especially to toggle between 3D and UI editing modes. + /// + internal void UpdateGizmoMode() + { + // Skip if gizmo mode was unmodified + if (_hasUILinked == _hasUILinkedCached) + return; + _hasUILinkedCached = _hasUILinked; + + if (_hasUILinked) + { + // UI widget + Gizmos.Active = null; + ViewportCamera = new UIEditorCamera { UIEditor = _uiRoot }; + + // Hide 3D visuals + ShowEditorPrimitives = false; + ShowDefaultSceneActors = false; + ShowDebugDraw = false; + + // Show whole UI on startup + ViewportCamera.ShowActor(Instance); + } + else + { + // Generic prefab + Gizmos.Active = TransformGizmo; + ViewportCamera = new FPSCamera(); + } + + // Update default components usage + bool defaultFeatures = !_hasUILinked; + _disableInputUpdate = _hasUILinked; + _spritesRenderer.Enabled = defaultFeatures; + SelectionOutline.Enabled = defaultFeatures; + _showDefaultSceneButton.Visible = defaultFeatures; + _cameraWidget.Visible = defaultFeatures; + _cameraButton.Visible = defaultFeatures; + _orthographicModeButton.Visible = defaultFeatures; + Task.Enabled = defaultFeatures; + UseAutomaticTaskManagement = defaultFeatures; + TintColor = defaultFeatures ? Color.White : Color.Transparent; + } + private void OnUpdate(float deltaTime) { + UpdateGizmoMode(); for (int i = 0; i < Gizmos.Count; i++) { Gizmos[i].Update(deltaTime); @@ -369,6 +440,13 @@ namespace FlaxEditor.Viewport _window.Spawn(actor); } + /// + public void OpenContextMenu() + { + var mouse = PointFromWindow(Root.MousePosition); + _window.ShowContextMenu(this, ref mouse); + } + /// protected override bool IsControllingMouse => Gizmos.Active?.IsControllingMouse ?? false; @@ -545,40 +623,6 @@ namespace FlaxEditor.Viewport } } - /// - public override void Draw() - { - base.Draw(); - - // Selected UI controls outline - bool drawAnySelectedControl = false; - // TODO: optimize this (eg. cache list of selected UIControl's when selection gets changed) - for (var i = 0; i < _window.Selection.Count; i++) - { - if (_window.Selection[i]?.EditableObject is UIControl controlActor && controlActor && controlActor.Control != null && controlActor.Control.VisibleInHierarchy && controlActor.Control.RootWindow != null) - { - if (!drawAnySelectedControl) - { - drawAnySelectedControl = true; - Render2D.PushTransform(ref _cachedTransform); - } - var control = controlActor.Control; - var bounds = control.EditorBounds; - var p1 = control.PointToParent(this, bounds.UpperLeft); - var p2 = control.PointToParent(this, bounds.UpperRight); - var p3 = control.PointToParent(this, bounds.BottomLeft); - var p4 = control.PointToParent(this, bounds.BottomRight); - var min = Float2.Min(Float2.Min(p1, p2), Float2.Min(p3, p4)); - var max = Float2.Max(Float2.Max(p1, p2), Float2.Max(p3, p4)); - bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero)); - var options = Editor.Instance.Options.Options.Visual; - Render2D.DrawRectangle(bounds, options.SelectionOutlineColor0, options.UISelectionOutlineSize); - } - } - if (drawAnySelectedControl) - Render2D.PopTransform(); - } - /// protected override void OnLeftMouseButtonUp() { diff --git a/Source/Editor/Viewport/Previews/AssetPreview.cs b/Source/Editor/Viewport/Previews/AssetPreview.cs index ee9ee3195..c460a24de 100644 --- a/Source/Editor/Viewport/Previews/AssetPreview.cs +++ b/Source/Editor/Viewport/Previews/AssetPreview.cs @@ -21,7 +21,7 @@ namespace FlaxEditor.Viewport.Previews /// public abstract class AssetPreview : EditorViewport, IEditorPrimitivesOwner { - private ContextMenuButton _showDefaultSceneButton; + internal ContextMenuButton _showDefaultSceneButton; private IntPtr _debugDrawContext; private bool _debugDrawEnable; private bool _editorPrimitivesEnable; diff --git a/Source/Editor/Viewport/Previews/PrefabPreview.cs b/Source/Editor/Viewport/Previews/PrefabPreview.cs index a17850557..2c7b60a1f 100644 --- a/Source/Editor/Viewport/Previews/PrefabPreview.cs +++ b/Source/Editor/Viewport/Previews/PrefabPreview.cs @@ -2,6 +2,7 @@ using System; using FlaxEngine; +using FlaxEngine.GUI; using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport.Previews @@ -14,7 +15,9 @@ namespace FlaxEditor.Viewport.Previews { private Prefab _prefab; private Actor _instance; - internal UIControl _uiControlLinked; + private UIControl _uiControlLinked; + internal bool _hasUILinked; + internal ContainerControl _uiParentLink; /// /// Gets or sets the prefab asset to preview. @@ -72,7 +75,7 @@ namespace FlaxEditor.Viewport.Previews // Unlink UI control if (_uiControlLinked) { - if (_uiControlLinked.Control?.Parent == this) + if (_uiControlLinked.Control?.Parent == _uiParentLink) _uiControlLinked.Control.Parent = null; _uiControlLinked = null; } @@ -82,6 +85,7 @@ namespace FlaxEditor.Viewport.Previews } _instance = value; + _hasUILinked = false; if (_instance) { @@ -103,20 +107,24 @@ namespace FlaxEditor.Viewport.Previews uiControl.Control != null && uiControl.Control.Parent == null) { - uiControl.Control.Parent = this; + uiControl.Control.Parent = _uiParentLink; _uiControlLinked = uiControl; + _hasUILinked = true; } } private void LinkCanvas(Actor actor) { if (actor is UICanvas uiCanvas) - uiCanvas.EditorOverride(Task, this); + { + uiCanvas.EditorOverride(Task, _uiParentLink); + if (uiCanvas.GUI.Parent == _uiParentLink) + _hasUILinked = true; + } + var children = actor.ChildrenCount; for (int i = 0; i < children; i++) - { LinkCanvas(actor.GetChild(i)); - } } /// @@ -126,6 +134,8 @@ namespace FlaxEditor.Viewport.Previews public PrefabPreview(bool useWidgets) : base(useWidgets) { + // Link to itself by default + _uiParentLink = this; } /// @@ -142,8 +152,6 @@ namespace FlaxEditor.Viewport.Previews /// public override void OnDestroy() { - if (IsDisposing) - return; Prefab = null; base.OnDestroy(); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index d7efb1260..21e037fbc 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -360,10 +360,9 @@ namespace FlaxEditor.Windows.Assets /// /// The parent control. /// The location (within a given control). - private void ShowContextMenu(Control parent, ref Float2 location) + internal void ShowContextMenu(Control parent, ref Float2 location) { var contextMenu = CreateContextMenu(); - contextMenu.Show(parent, location); } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index eb5070cb1..b788821cf 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -344,6 +344,7 @@ namespace FlaxEditor.Windows.Assets private void OnPrefabOpened() { _viewport.Prefab = _asset; + _viewport.UpdateGizmoMode(); Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); @@ -359,7 +360,7 @@ namespace FlaxEditor.Windows.Assets try { - Editor.Scene.OnSaveStart(_viewport); + Editor.Scene.OnSaveStart(_viewport._uiParentLink); // Simply update changes Editor.Prefabs.ApplyAll(_viewport.Instance); @@ -379,7 +380,7 @@ namespace FlaxEditor.Windows.Assets } finally { - Editor.Scene.OnSaveEnd(_viewport); + Editor.Scene.OnSaveEnd(_viewport._uiParentLink); } } diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index f1ea43d9b..71addb3ed 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Xml; +using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEditor.Options; @@ -194,133 +195,14 @@ namespace FlaxEditor.Windows public bool Active; } - private class GameRoot : ContainerControl + /// + /// Root control for game UI preview in Editor. Supports basic UI editing via . + /// + private class GameRoot : UIEditorRoot { - public bool EnableEvents => !Time.GamePaused; - - public override bool RayCast(ref Float2 location, out Control hit) - { - return RayCastChildren(ref location, out hit); - } - - public override bool ContainsPoint(ref Float2 location, bool precise = false) - { - if (precise) - return false; - return base.ContainsPoint(ref location, precise); - } - - public override bool OnCharInput(char c) - { - if (!EnableEvents) - return false; - - return base.OnCharInput(c); - } - - public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) - { - if (!EnableEvents) - return DragDropEffect.None; - - return base.OnDragDrop(ref location, data); - } - - public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) - { - if (!EnableEvents) - return DragDropEffect.None; - - return base.OnDragEnter(ref location, data); - } - - public override void OnDragLeave() - { - if (!EnableEvents) - return; - - base.OnDragLeave(); - } - - public override DragDropEffect OnDragMove(ref Float2 location, DragData data) - { - if (!EnableEvents) - return DragDropEffect.None; - - return base.OnDragMove(ref location, data); - } - - public override bool OnKeyDown(KeyboardKeys key) - { - if (!EnableEvents) - return false; - - return base.OnKeyDown(key); - } - - public override void OnKeyUp(KeyboardKeys key) - { - if (!EnableEvents) - return; - - base.OnKeyUp(key); - } - - public override bool OnMouseDoubleClick(Float2 location, MouseButton button) - { - if (!EnableEvents) - return false; - - return base.OnMouseDoubleClick(location, button); - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (!EnableEvents) - return false; - - return base.OnMouseDown(location, button); - } - - public override void OnMouseEnter(Float2 location) - { - if (!EnableEvents) - return; - - base.OnMouseEnter(location); - } - - public override void OnMouseLeave() - { - if (!EnableEvents) - return; - - base.OnMouseLeave(); - } - - public override void OnMouseMove(Float2 location) - { - if (!EnableEvents) - return; - - base.OnMouseMove(location); - } - - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (!EnableEvents) - return false; - - return base.OnMouseUp(location, button); - } - - public override bool OnMouseWheel(Float2 location, float delta) - { - if (!EnableEvents) - return false; - - return base.OnMouseWheel(location, delta); - } + public override bool EnableInputs => !Time.GamePaused; + public override bool EnableSelecting => !Editor.IsPlayMode || Time.GamePaused; + public override TransformGizmo TransformGizmo => Editor.Instance.MainTransformGizmo; } /// @@ -348,13 +230,9 @@ namespace FlaxEditor.Windows // Override the game GUI root _guiRoot = new GameRoot { - AnchorPreset = AnchorPresets.StretchAll, - Offsets = Margin.Zero, - //Visible = false, - AutoFocus = false, Parent = _viewport }; - RootControl.GameRoot = _guiRoot; + RootControl.GameRoot = _guiRoot.UIRoot; SizeChanged += control => { ResizeViewport(); }; @@ -916,35 +794,6 @@ namespace FlaxEditor.Windows Render2D.DrawText(style.FontLarge, "No camera", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); } - // Selected UI controls outline - bool drawAnySelectedControl = false; - // TODO: optimize this (eg. cache list of selected UIControl's when selection gets changed) - var selection = Editor.SceneEditing.Selection; - for (var i = 0; i < selection.Count; i++) - { - if (selection[i].EditableObject is UIControl controlActor && controlActor && controlActor.Control != null && controlActor.Control.VisibleInHierarchy && controlActor.Control.RootWindow != null) - { - if (!drawAnySelectedControl) - { - drawAnySelectedControl = true; - Render2D.PushTransform(ref _viewport._cachedTransform); - } - var options = Editor.Options.Options.Visual; - var control = controlActor.Control; - var bounds = control.EditorBounds; - var p1 = control.PointToParent(_viewport, bounds.UpperLeft); - var p2 = control.PointToParent(_viewport, bounds.UpperRight); - var p3 = control.PointToParent(_viewport, bounds.BottomLeft); - var p4 = control.PointToParent(_viewport, bounds.BottomRight); - var min = Float2.Min(Float2.Min(p1, p2), Float2.Min(p3, p4)); - var max = Float2.Max(Float2.Max(p1, p2), Float2.Max(p3, p4)); - bounds = new Rectangle(min, Float2.Max(max - min, Float2.Zero)); - Render2D.DrawRectangle(bounds, options.SelectionOutlineColor0, options.UISelectionOutlineSize); - } - } - if (drawAnySelectedControl) - Render2D.PopTransform(); - // Play mode hints and overlay if (Editor.StateMachine.IsPlayMode) { diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index 4ff4752ac..f4a49195f 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -1,7 +1,6 @@ // Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; -using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.SceneGraph; @@ -258,10 +257,9 @@ namespace FlaxEditor.Windows /// /// The parent control. /// The location (within a given control). - private void ShowContextMenu(Control parent, Float2 location) + internal void ShowContextMenu(Control parent, Float2 location) { var contextMenu = CreateContextMenu(); - contextMenu.Show(parent, location); } diff --git a/Source/Engine/UI/GUI/Control.Bounds.cs b/Source/Engine/UI/GUI/Control.Bounds.cs index ec96f89f2..cbb3ba80b 100644 --- a/Source/Engine/UI/GUI/Control.Bounds.cs +++ b/Source/Engine/UI/GUI/Control.Bounds.cs @@ -383,7 +383,7 @@ namespace FlaxEngine.GUI /// /// Gets or sets the shear transform angles (x, y). Defined in degrees. Shearing happens relative to the control pivot point. /// - [DefaultValue(0.0f)] + [DefaultValue(typeof(Float2), "0,0")] [ExpandGroups, EditorDisplay("Transform"), EditorOrder(1040), Tooltip("The shear transform angles (x, y). Defined in degrees. Shearing happens relative to the control pivot point.")] public Float2 Shear {