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