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
{