diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings
index 0808c4488..655924b88 100644
--- a/Flax.sln.DotSettings
+++ b/Flax.sln.DotSettings
@@ -323,6 +323,7 @@
True
True
True
+ True
True
True
True
diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs
index 0b9b536ff..8e252a207 100644
--- a/Source/Editor/CustomEditors/CustomEditor.cs
+++ b/Source/Editor/CustomEditors/CustomEditor.cs
@@ -40,6 +40,11 @@ namespace FlaxEditor.CustomEditors
[HideInEditor]
public abstract class CustomEditor
{
+ ///
+ /// True if Editor is during value setting (eg. by user or from copy/paste).
+ ///
+ public static bool IsSettingValue = false;
+
private LayoutElementsContainer _layout;
private CustomEditorPresenter _presenter;
private CustomEditor _parent;
@@ -266,23 +271,30 @@ namespace FlaxEditor.CustomEditors
// Check if need to update value
if (_hasValueDirty)
{
- // Cleanup (won't retry update in case of exception)
- object val = _valueToSet;
- _hasValueDirty = false;
- _valueToSet = null;
-
- // Assign value
- SynchronizeValue(val);
-
- // Propagate values up (eg. when member of structure gets modified, also structure should be updated as a part of the other object)
- var obj = _parent;
- while (obj._parent != null && !(obj._parent is SyncPointEditor))
+ IsSettingValue = true;
+ try
{
- obj.Values.Set(obj._parent.Values, obj.Values);
- obj = obj._parent;
- }
+ // Cleanup (won't retry update in case of exception)
+ object val = _valueToSet;
+ _hasValueDirty = false;
+ _valueToSet = null;
- OnUnDirty();
+ // Assign value
+ SynchronizeValue(val);
+
+ // Propagate values up (eg. when member of structure gets modified, also structure should be updated as a part of the other object)
+ var obj = _parent;
+ while (obj._parent != null && !(obj._parent is SyncPointEditor))
+ {
+ obj.Values.Set(obj._parent.Values, obj.Values);
+ obj = obj._parent;
+ }
+ }
+ finally
+ {
+ OnUnDirty();
+ IsSettingValue = false;
+ }
}
else
{
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index aaed484a1..7b815bc98 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -817,6 +817,7 @@ namespace FlaxEditor.Windows
// 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 < Editor.Instance.SceneEditing.Selection.Count; i++)
{
if (Editor.Instance.SceneEditing.Selection[i].EditableObject is UIControl controlActor && controlActor && controlActor.Control != null)
@@ -827,7 +828,8 @@ namespace FlaxEditor.Windows
Render2D.PushTransform(ref _viewport._cachedTransform);
}
var control = controlActor.Control;
- var bounds = Rectangle.FromPoints(control.PointToParent(_viewport, Float2.Zero), control.PointToParent(_viewport, control.Size));
+ var bounds = control.EditorBounds;
+ bounds = Rectangle.FromPoints(control.PointToParent(_viewport, bounds.Location), control.PointToParent(_viewport, bounds.Size));
Render2D.DrawRectangle(bounds, Editor.Instance.Options.Options.Visual.SelectionOutlineColor0, Editor.Instance.Options.Options.Visual.UISelectionOutlineSize);
}
}
diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs
new file mode 100644
index 000000000..4c670864b
--- /dev/null
+++ b/Source/Engine/UI/GUI/CanvasScaler.cs
@@ -0,0 +1,526 @@
+// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+
+using System.ComponentModel;
+
+namespace FlaxEngine.GUI
+{
+ ///
+ /// UI canvas scaling component for user interface that targets multiple different game resolutions (eg. mobile screens).
+ ///
+ public class CanvasScaler : ContainerControl
+ {
+ ///
+ /// Canvas scaling modes.
+ ///
+ public enum ScalingMode
+ {
+ ///
+ /// Applies constant scale to the whole UI in pixels.
+ ///
+ ConstantPixelSize,
+
+ ///
+ /// Applies constant scale to the whole UI in physical units (depends on the screen DPI). Ensures the UI will have specific real-world size no matter the screen resolution.
+ ///
+ ConstantPhysicalSize,
+
+ ///
+ /// Applies min/max scaling to the UI depending on the screen resolution. Ensures the UI size won't go below min or above max resolution to maintain it's readability.
+ ///
+ ScaleWithResolution,
+
+ ///
+ /// Applies scaling curve to the UI depending on the screen DPI.
+ ///
+ ScaleWithDpi,
+ }
+
+ ///
+ /// Physical unit types for canvas scaling.
+ ///
+ public enum PhysicalUnitMode
+ {
+ ///
+ /// Centimeters (0.01 meter).
+ ///
+ Centimeters,
+
+ ///
+ /// Millimeters (0.1 centimeter, 0.001 meter).
+ ///
+ Millimeters,
+
+ ///
+ /// Inches (2.54 centimeters).
+ ///
+ Inches,
+
+ ///
+ /// Points (1/72 inch, 1/112 of pica).
+ ///
+ Points,
+
+ ///
+ /// Pica (1/6 inch).
+ ///
+ Picas,
+ }
+
+ ///
+ /// Resolution scaling modes.
+ ///
+ public enum ResolutionScalingMode
+ {
+ ///
+ /// Uses the shortest side of the screen to scale the canvas for min/max rule.
+ ///
+ ShortestSide,
+
+ ///
+ /// Uses the longest side of the screen to scale the canvas for min/max rule.
+ ///
+ LongestSide,
+
+ ///
+ /// Uses the horizontal (X, width) side of the screen to scale the canvas for min/max rule.
+ ///
+ Horizontal,
+
+ ///
+ /// Uses the vertical (Y, height) side of the screen to scale the canvas for min/max rule.
+ ///
+ Vertical,
+ }
+
+ private ScalingMode _scalingMode = ScalingMode.ConstantPixelSize;
+ private PhysicalUnitMode _physicalUnit = PhysicalUnitMode.Points;
+ private ResolutionScalingMode _resolutionMode = ResolutionScalingMode.ShortestSide;
+ private float _scale = 1.0f;
+ private float _scaleFactor = 1.0f;
+ private float _physicalUnitSize = 1.0f;
+ private Float2 _resolutionMin = new Float2(1, 1);
+ private Float2 _resolutionMax = new Float2(10000, 10000);
+
+ ///
+ /// Gets the current UI scale. Computed based on the setup when performing layout.
+ ///
+ public float CurrentScale => _scale;
+
+ ///
+ /// The UI Canvas scaling mode.
+ ///
+ [EditorOrder(0), EditorDisplay("Canvas Scaler"), ExpandGroups, DefaultValue(ScalingMode.ConstantPixelSize)]
+ public ScalingMode Scaling
+ {
+ get => _scalingMode;
+ set
+ {
+ if (_scalingMode == value)
+ return;
+ _scalingMode = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas scale. Applied in all scaling modes for custom UI sizing.
+ ///
+ [EditorOrder(10), EditorDisplay("Canvas Scaler"), DefaultValue(1.0f), Limit(0.001f, 1000.0f, 0.01f)]
+ public float ScaleFactor
+ {
+ get => _scaleFactor;
+ set
+ {
+ if (Mathf.NearEqual(_scaleFactor, value))
+ return;
+ _scaleFactor = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas physical unit to use for scaling via PhysicalUnitSize. Used only in ConstantPhysicalSize mode.
+ ///
+ [EditorOrder(100), EditorDisplay("Canvas Scaler"), DefaultValue(PhysicalUnitMode.Points), VisibleIf(nameof(IsConstantPhysicalSize))]
+ public PhysicalUnitMode PhysicalUnit
+ {
+ get => _physicalUnit;
+ set
+ {
+ if (_physicalUnit == value)
+ return;
+ _physicalUnit = value;
+#if FLAX_EDITOR
+ if (FlaxEditor.CustomEditors.CustomEditor.IsSettingValue)
+ {
+ // Set auto-default physical unit value for easier tweaking in Editor
+ _physicalUnitSize = GetUnitDpi(_physicalUnit) / Platform.Dpi;
+ }
+#endif
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas physical unit value. Used only in ConstantPhysicalSize mode.
+ ///
+ [EditorOrder(110), EditorDisplay("Canvas Scaler"), DefaultValue(1.0f), Limit(0.000001f, 1000000.0f, 0.0f), VisibleIf(nameof(IsConstantPhysicalSize))]
+ public float PhysicalUnitSize
+ {
+ get => _physicalUnitSize;
+ set
+ {
+ if (Mathf.NearEqual(_physicalUnitSize, value))
+ return;
+ _physicalUnitSize = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas resolution scaling mode. Controls min/max resolutions usage in relation to the current screen resolution to compute the UI scale. Used only in ScaleWithResolution mode.
+ ///
+ [EditorOrder(120), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))]
+ public ResolutionScalingMode ResolutionMode
+ {
+ get => _resolutionMode;
+ set
+ {
+ if (_resolutionMode == value)
+ return;
+ _resolutionMode = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas minimum resolution. If the screen has lower size, then the interface will be scaled accordingly. Used only in ScaleWithResolution mode.
+ ///
+ [EditorOrder(120), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))]
+ public Float2 ResolutionMin
+ {
+ get => _resolutionMin;
+ set
+ {
+ value = Float2.Max(value, Float2.One);
+ if (Float2.NearEqual(ref _resolutionMin, ref value))
+ return;
+ _resolutionMin = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas maximum resolution. If the screen has higher size, then the interface will be scaled accordingly. Used only in ScaleWithResolution mode.
+ ///
+ [EditorOrder(130), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))]
+ public Float2 ResolutionMax
+ {
+ get => _resolutionMax;
+ set
+ {
+ value = Float2.Max(value, Float2.One);
+ if (Float2.NearEqual(ref _resolutionMax, ref value))
+ return;
+ _resolutionMax = value;
+ PerformLayout();
+ }
+ }
+
+ ///
+ /// The UI Canvas scaling curve based on screen resolution - shortest/longest/vertical/horizontal (key is resolution, value is scale factor). Clear keyframes to skip using it and follow min/max rules only. Used only in ScaleWithResolution mode.
+ ///
+ [EditorOrder(140), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))]
+ public LinearCurve ResolutionCurve = new LinearCurve(new[]
+ {
+ new LinearCurve.Keyframe(480, 0.444f), // 480p
+ new LinearCurve.Keyframe(720, 0.666f), // 720p
+ new LinearCurve.Keyframe(1080, 1.0f), // 1080p
+ new LinearCurve.Keyframe(8640, 8.0f), // 8640p
+ });
+
+ ///
+ /// The UI Canvas scaling curve based on screen DPI (key is DPI, value is scale factor). Used only in ScaleWithDpi mode.
+ ///
+ [EditorOrder(150), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithDpi))]
+ public LinearCurve DpiCurve = new LinearCurve(new[]
+ {
+ new LinearCurve.Keyframe(1.0f, 1.0f),
+ new LinearCurve.Keyframe(96.0f, 1.0f),
+ new LinearCurve.Keyframe(200.0f, 2.0f),
+ new LinearCurve.Keyframe(400.0f, 4.0f),
+ });
+
+#if FLAX_EDITOR
+ private bool IsConstantPhysicalSize => _scalingMode == ScalingMode.ConstantPhysicalSize;
+ private bool IsScaleWithResolution => _scalingMode == ScalingMode.ScaleWithResolution;
+ private bool IsScaleWithDpi => _scalingMode == ScalingMode.ScaleWithDpi;
+#endif
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CanvasScaler()
+ {
+ // Fill the canvas by default
+ Offsets = Margin.Zero;
+ AnchorPreset = AnchorPresets.StretchAll;
+ }
+
+ ///
+ /// Updates the scaler for the current setup.
+ ///
+ public void UpdateScale()
+ {
+ float scale = 1.0f;
+ if (Parent != null)
+ {
+ UICanvas canvas = (Root as CanvasRootControl)?.Canvas;
+ float dpi = Platform.Dpi;
+ switch (canvas?.RenderMode ?? CanvasRenderMode.ScreenSpace)
+ {
+ case CanvasRenderMode.WorldSpace:
+ case CanvasRenderMode.WorldSpaceFaceCamera:
+ scale = 1.0f;
+ break;
+ default:
+ switch (_scalingMode)
+ {
+ case ScalingMode.ConstantPixelSize:
+ scale = 1.0f;
+ break;
+ case ScalingMode.ConstantPhysicalSize:
+ {
+ float targetDpi = GetUnitDpi(_physicalUnit);
+ scale = dpi / targetDpi * _physicalUnitSize;
+ break;
+ }
+ case ScalingMode.ScaleWithResolution:
+ {
+ Float2 resolution = Float2.Max(Size, Float2.One);
+ int axis = 0;
+ switch (_resolutionMode)
+ {
+ case ResolutionScalingMode.ShortestSide:
+ axis = resolution.X > resolution.Y ? 1 : 0;
+ break;
+ case ResolutionScalingMode.LongestSide:
+ axis = resolution.X > resolution.Y ? 0 : 1;
+ break;
+ case ResolutionScalingMode.Horizontal:
+ axis = 0;
+ break;
+ case ResolutionScalingMode.Vertical:
+ axis = 1;
+ break;
+ }
+ float min = _resolutionMin[axis], max = _resolutionMax[axis], value = resolution[axis];
+ if (value < min)
+ scale = min / value;
+ else if (value > max)
+ scale = max / value;
+ if (ResolutionCurve != null && ResolutionCurve.Keyframes?.Length != 0)
+ {
+ ResolutionCurve.Evaluate(out var curveScale, value, false);
+ scale *= curveScale;
+ }
+ break;
+ }
+ case ScalingMode.ScaleWithDpi:
+ DpiCurve?.Evaluate(out scale, dpi, false);
+ break;
+ }
+ break;
+ }
+ }
+ _scale = Mathf.Max(scale * _scaleFactor, 0.01f);
+ }
+
+ private float GetUnitDpi(PhysicalUnitMode unit)
+ {
+ float dpi = 1.0f;
+ switch (unit)
+ {
+ case PhysicalUnitMode.Centimeters:
+ dpi = 2.54f;
+ break;
+ case PhysicalUnitMode.Millimeters:
+ dpi = 25.4f;
+ break;
+ case PhysicalUnitMode.Inches:
+ dpi = 1;
+ break;
+ case PhysicalUnitMode.Points:
+ dpi = 72;
+ break;
+ case PhysicalUnitMode.Picas:
+ dpi = 6;
+ break;
+ }
+ return dpi;
+ }
+
+ ///
+ protected override void PerformLayoutBeforeChildren()
+ {
+ // Update current scaling before performing layout
+ UpdateScale();
+
+ base.PerformLayoutBeforeChildren();
+ }
+
+ #region UI Scale
+
+#if FLAX_EDITOR
+ ///
+ public override Rectangle EditorBounds => new Rectangle(Float2.Zero, Size / _scale);
+#endif
+
+ ///
+ public override void Draw()
+ {
+ DrawSelf();
+
+ // Draw children with scale
+ var scaling = new Float3(_scale, _scale, 1);
+ Matrix3x3.Scaling(ref scaling, out Matrix3x3 scale);
+ Render2D.PushTransform(scale);
+ if (ClipChildren)
+ {
+ GetDesireClientArea(out var clientArea);
+ Render2D.PushClip(ref clientArea);
+ DrawChildren();
+ Render2D.PopClip();
+ }
+ else
+ {
+ DrawChildren();
+ }
+ Render2D.PopTransform();
+ }
+
+ ///
+ public override void GetDesireClientArea(out Rectangle rect)
+ {
+ // Scale the area for the client controls
+ rect = new Rectangle(Float2.Zero, Size / _scale);
+ }
+
+ ///
+ public override bool IntersectsContent(ref Float2 locationParent, out Float2 location)
+ {
+ // Skip local PointFromParent but use base code
+ location = base.PointFromParent(ref locationParent);
+ return ContainsPoint(ref location);
+ }
+
+ ///
+ public override Float2 PointToParent(ref Float2 location)
+ {
+ var result = base.PointToParent(ref location);
+ result *= _scaleFactor;
+ return result;
+ }
+
+ ///
+ public override Float2 PointFromParent(ref Float2 location)
+ {
+ var result = base.PointFromParent(ref location);
+ result /= _scaleFactor;
+ return result;
+ }
+
+ ///
+ public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
+ {
+ location /= _scale;
+ return base.OnDragEnter(ref location, data);
+ }
+
+ ///
+ public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
+ {
+ location /= _scale;
+ return base.OnDragMove(ref location, data);
+ }
+
+ ///
+ public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
+ {
+ location /= _scale;
+ return base.OnDragDrop(ref location, data);
+ }
+
+ ///
+ public override void OnMouseEnter(Float2 location)
+ {
+ location /= _scale;
+ base.OnMouseEnter(location);
+ }
+
+ ///
+ public override void OnMouseMove(Float2 location)
+ {
+ location /= _scale;
+ base.OnMouseMove(location);
+ }
+
+ ///
+ public override bool OnMouseDown(Float2 location, MouseButton button)
+ {
+ location /= _scale;
+ return base.OnMouseDown(location, button);
+ }
+
+ ///
+ public override bool OnMouseUp(Float2 location, MouseButton button)
+ {
+ location /= _scale;
+ return base.OnMouseUp(location, button);
+ }
+
+ ///
+ public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
+ {
+ location /= _scale;
+ return base.OnMouseDoubleClick(location, button);
+ }
+
+ ///
+ public override bool OnMouseWheel(Float2 location, float delta)
+ {
+ location /= _scale;
+ return base.OnMouseWheel(location, delta);
+ }
+
+ ///
+ public override void OnTouchEnter(Float2 location, int pointerId)
+ {
+ location /= _scale;
+ base.OnTouchEnter(location, pointerId);
+ }
+
+ ///
+ public override void OnTouchMove(Float2 location, int pointerId)
+ {
+ location /= _scale;
+ base.OnTouchMove(location, pointerId);
+ }
+
+ ///
+ public override bool OnTouchDown(Float2 location, int pointerId)
+ {
+ location /= _scale;
+ return base.OnTouchDown(location, pointerId);
+ }
+
+ ///
+ public override bool OnTouchUp(Float2 location, int pointerId)
+ {
+ location /= _scale;
+ return base.OnTouchUp(location, pointerId);
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Engine/UI/GUI/Control.cs b/Source/Engine/UI/GUI/Control.cs
index d7b226cd1..8b2401428 100644
--- a/Source/Engine/UI/GUI/Control.cs
+++ b/Source/Engine/UI/GUI/Control.cs
@@ -1259,6 +1259,13 @@ namespace FlaxEngine.GUI
return PointFromParent(ref location);
}
+#if FLAX_EDITOR
+ ///
+ /// Bounds rectangle for editor UI.
+ ///
+ public virtual Rectangle EditorBounds => new Rectangle(Float2.Zero, _bounds.Size);
+#endif
+
#endregion
#region Control Action