// Copyright (c) 2012-2024 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). /// [ActorToolbox("GUI")] 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(1f, 1f); private Float2 _resolutionMax = new Float2(7680f, 4320f); /// /// 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. /// #if FLAX_EDITOR [EditorOrder(100), EditorDisplay("Canvas Scaler"), DefaultValue(PhysicalUnitMode.Points), VisibleIf(nameof(IsConstantPhysicalSize))] #endif 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. /// #if FLAX_EDITOR [EditorOrder(110), EditorDisplay("Canvas Scaler"), DefaultValue(1.0f), Limit(0.000001f, 1000000.0f, 0.0f), VisibleIf(nameof(IsConstantPhysicalSize))] #endif 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. /// #if FLAX_EDITOR [EditorOrder(120), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))] #endif 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. /// #if FLAX_EDITOR [EditorOrder(120), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))] #endif 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. /// #if FLAX_EDITOR [EditorOrder(130), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))] #endif 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. /// #if FLAX_EDITOR [EditorOrder(140), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithResolution))] #endif public LinearCurve ResolutionCurve = new LinearCurve(new[] { new LinearCurve.Keyframe(0f, 0f), // 0p new LinearCurve.Keyframe(480f, 0.444f), // 480p new LinearCurve.Keyframe(720f, 0.666f), // 720p new LinearCurve.Keyframe(1080f, 1.0f), // 1080p new LinearCurve.Keyframe(8640f, 8.0f), // 8640p }); /// /// The UI Canvas scaling curve based on screen DPI (key is DPI, value is scale factor). Used only in ScaleWithDpi mode. /// #if FLAX_EDITOR [EditorOrder(150), EditorDisplay("Canvas Scaler"), VisibleIf(nameof(IsScaleWithDpi))] #endif 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; AutoFocus = false; } /// /// 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 != 0f) { 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 = 1f; break; case PhysicalUnitMode.Points: dpi = 72f; break; case PhysicalUnitMode.Picas: dpi = 6f; 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 bool IntersectsChildContent(Control child, Float2 location, out Float2 childSpaceLocation) { location /= _scale; return base.IntersectsChildContent(child, location, out childSpaceLocation); } /// public override bool ContainsPoint(ref Float2 location, bool precise = false) { if (precise) // Ignore as utility-only element return false; return base.ContainsPoint(ref location, precise); } /// public override bool RayCast(ref Float2 location, out Control hit) { var p = location / _scale; if (RayCastChildren(ref p, out hit)) return true; return base.RayCast(ref location, out hit); } /// public override Float2 PointToParent(ref Float2 location) { var result = base.PointToParent(ref location); result *= _scale; return result; } /// public override Float2 PointFromParent(ref Float2 location) { var result = base.PointFromParent(ref location); result /= _scale; return result; } #endregion } }