diff --git a/OpenRA.Game/Graphics/PlatformInterfaces.cs b/OpenRA.Game/Graphics/PlatformInterfaces.cs index 5fe42212fe..4898ec5538 100644 --- a/OpenRA.Game/Graphics/PlatformInterfaces.cs +++ b/OpenRA.Game/Graphics/PlatformInterfaces.cs @@ -57,6 +57,7 @@ namespace OpenRA IHardwareCursor CreateHardwareCursor(string name, Size size, byte[] data, int2 hotspot, bool pixelDouble); void SetHardwareCursor(IHardwareCursor cursor); void SetRelativeMouseMode(bool mode); + void SetScaleModifier(float scale); } public interface IGraphicsContext : IDisposable diff --git a/OpenRA.Game/Renderer.cs b/OpenRA.Game/Renderer.cs index cc10626535..43722b0e14 100644 --- a/OpenRA.Game/Renderer.cs +++ b/OpenRA.Game/Renderer.cs @@ -91,6 +91,11 @@ namespace OpenRA return new Size(size.X, size.Y); } + public void SetUIScale(float scale) + { + Window.SetScaleModifier(scale); + } + public void InitializeFonts(ModData modData) { if (Fonts != null) diff --git a/OpenRA.Game/WorldViewportSizes.cs b/OpenRA.Game/WorldViewportSizes.cs index fa3247badb..b951bc6406 100644 --- a/OpenRA.Game/WorldViewportSizes.cs +++ b/OpenRA.Game/WorldViewportSizes.cs @@ -10,6 +10,7 @@ #endregion using System.Collections.Generic; +using OpenRA.Primitives; namespace OpenRA { @@ -23,6 +24,8 @@ namespace OpenRA public readonly int MaxZoomWindowHeight = 240; public readonly bool AllowNativeZoom = true; + public readonly Size MinEffectiveResolution = new Size(1024, 720); + public int2 GetSizeRange(WorldViewport distance) { return distance == WorldViewport.Close ? CloseWindowHeights diff --git a/OpenRA.Mods.Common/LoadScreens/BlankLoadScreen.cs b/OpenRA.Mods.Common/LoadScreens/BlankLoadScreen.cs index dd48dd178d..139121ad6c 100644 --- a/OpenRA.Mods.Common/LoadScreens/BlankLoadScreen.cs +++ b/OpenRA.Mods.Common/LoadScreens/BlankLoadScreen.cs @@ -112,6 +112,15 @@ namespace OpenRA.Mods.Common.LoadScreens public virtual bool BeforeLoad() { + // Reset the UI scaling if the user has configured a UI scale that pushes us below the minimum allowed effective resolution + var minResolution = ModData.Manifest.Get().MinEffectiveResolution; + var resolution = Game.Renderer.Resolution; + if ((resolution.Width < minResolution.Width || resolution.Height < minResolution.Height) && Game.Settings.Graphics.UIScale > 1.0f) + { + Game.Settings.Graphics.UIScale = 1.0f; + Game.Renderer.SetUIScale(1.0f); + } + // If a ModContent section is defined then we need to make sure that the // required content is installed or switch to the defined content installer. if (!ModData.Manifest.Contains()) diff --git a/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs index 64228a1b5d..825fd06e8c 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/SettingsLogic.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Support; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -34,6 +35,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly ModData modData; readonly WorldRenderer worldRenderer; + readonly WorldViewportSizes viewportSizes; readonly Dictionary logicArgs; SoundDevice soundDevice; @@ -61,6 +63,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic this.worldRenderer = worldRenderer; this.modData = modData; this.logicArgs = logicArgs; + viewportSizes = modData.Manifest.Get(); panelContainer = widget.Get("SETTINGS_PANEL"); tabContainer = widget.Get("TAB_CONTAINER"); @@ -261,7 +264,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var battlefieldCameraDropDown = panel.Get("BATTLEFIELD_CAMERA_DROPDOWN"); var battlefieldCameraLabel = new CachedTransform(vs => ViewportSizeNames[vs]); - battlefieldCameraDropDown.OnMouseDown = _ => ShowBattlefieldCameraDropdown(battlefieldCameraDropDown, ds); + battlefieldCameraDropDown.OnMouseDown = _ => ShowBattlefieldCameraDropdown(battlefieldCameraDropDown, viewportSizes, ds); battlefieldCameraDropDown.GetText = () => battlefieldCameraLabel.Update(ds.ViewportDistance); // Update vsync immediately @@ -273,6 +276,19 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.Renderer.SetVSyncEnabled(ds.VSync); }; + var uiScaleDropdown = panel.Get("UI_SCALE_DROPDOWN"); + var uiScaleLabel = new CachedTransform(s => "{0}%".F((int)(100 * s))); + uiScaleDropdown.OnMouseDown = _ => ShowUIScaleDropdown(uiScaleDropdown, ds); + uiScaleDropdown.GetText = () => uiScaleLabel.Update(ds.UIScale); + + var minResolution = viewportSizes.MinEffectiveResolution; + var resolution = Game.Renderer.Resolution; + var disableUIScale = worldRenderer.World.Type != WorldType.Shellmap || + resolution.Width * ds.UIScale < 1.25f * minResolution.Width || + resolution.Height * ds.UIScale < 1.25f * minResolution.Height; + + uiScaleDropdown.IsDisabled = () => disableUIScale; + panel.Get("WINDOW_RESOLUTION").IsVisible = () => ds.Mode == WindowMode.Windowed; var windowWidth = panel.Get("WINDOW_WIDTH"); var origWidthText = windowWidth.Text = ds.WindowedSize.X.ToString(); @@ -357,6 +373,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic ds.CursorDouble = dds.CursorDouble; ds.ViewportDistance = dds.ViewportDistance; + if (ds.UIScale != dds.UIScale) + { + var oldScale = ds.UIScale; + ds.UIScale = dds.UIScale; + Game.Renderer.SetUIScale(dds.UIScale); + RecalculateWidgetLayout(Ui.Root); + Viewport.LastMousePos = (Viewport.LastMousePos.ToFloat2() * oldScale / ds.UIScale).ToInt2(); + } + ps.Color = dps.Color; ps.Name = dps.Name; }; @@ -827,7 +852,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, options.Keys, setupItem); } - static void ShowBattlefieldCameraDropdown(DropDownButtonWidget dropdown, GraphicSettings gs) + static void ShowBattlefieldCameraDropdown(DropDownButtonWidget dropdown, WorldViewportSizes viewportSizes, GraphicSettings gs) { Func setupItem = (o, itemTemplate) => { @@ -840,7 +865,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic return item; }; - var viewportSizes = Game.ModData.Manifest.Get(); var windowHeight = Game.Renderer.NativeResolution.Height; var validSizes = new List() { WorldViewport.Close }; @@ -857,6 +881,78 @@ namespace OpenRA.Mods.Common.Widgets.Logic dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, validSizes, setupItem); } + static void RecalculateWidgetLayout(Widget w, bool insideScrollPanel = false) + { + // HACK: Recalculate the widget bounds to fit within the new effective window bounds + // This is fragile, and only works when called when Settings is opened via the main menu. + + // HACK: Skip children badges container on the main menu + // This has a fixed size, with calculated size and children positions that break if we adjust them here + if (w.Id == "BADGES_CONTAINER") + return; + + var parentBounds = w.Parent == null + ? new Rectangle(0, 0, Game.Renderer.Resolution.Width, Game.Renderer.Resolution.Height) + : w.Parent.Bounds; + + var substitutions = new Dictionary(); + substitutions.Add("WINDOW_RIGHT", Game.Renderer.Resolution.Width); + substitutions.Add("WINDOW_BOTTOM", Game.Renderer.Resolution.Height); + substitutions.Add("PARENT_RIGHT", parentBounds.Width); + substitutions.Add("PARENT_LEFT", parentBounds.Left); + substitutions.Add("PARENT_TOP", parentBounds.Top); + substitutions.Add("PARENT_BOTTOM", parentBounds.Height); + + var width = Evaluator.Evaluate(w.Width, substitutions); + var height = Evaluator.Evaluate(w.Height, substitutions); + + substitutions.Add("WIDTH", width); + substitutions.Add("HEIGHT", height); + + if (insideScrollPanel) + w.Bounds = new Rectangle(w.Bounds.X, w.Bounds.Y, width, w.Bounds.Height); + else + w.Bounds = new Rectangle(Evaluator.Evaluate(w.X, substitutions), + Evaluator.Evaluate(w.Y, substitutions), + width, + height); + + foreach (var c in w.Children) + RecalculateWidgetLayout(c, insideScrollPanel || w is ScrollPanelWidget); + } + + static void ShowUIScaleDropdown(DropDownButtonWidget dropdown, GraphicSettings gs) + { + Func setupItem = (o, itemTemplate) => + { + var item = ScrollItemWidget.Setup(itemTemplate, + () => gs.UIScale == o, + () => + { + Game.RunAfterTick(() => + { + var oldScale = gs.UIScale; + gs.UIScale = o; + + Game.Renderer.SetUIScale(o); + RecalculateWidgetLayout(Ui.Root); + Viewport.LastMousePos = (Viewport.LastMousePos.ToFloat2() * oldScale / gs.UIScale).ToInt2(); + }); + }); + + var label = "{0}%".F((int)(100 * o)); + item.Get("LABEL").GetText = () => label; + return item; + }; + + var viewportSizes = Game.ModData.Manifest.Get(); + var maxScales = new float2(Game.Renderer.NativeResolution) / new float2(viewportSizes.MinEffectiveResolution); + var maxScale = Math.Min(maxScales.X, maxScales.Y); + + var validScales = new[] { 1f, 1.25f, 1.5f, 1.75f, 2f }.Where(x => x <= maxScale); + dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, validScales, setupItem); + } + void MakeMouseFocusSettingsLive() { var gameSettings = Game.Settings.Game; diff --git a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs index 0c0d1b537d..bb6c153689 100644 --- a/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs +++ b/OpenRA.Platforms.Default/Sdl2PlatformWindow.cs @@ -443,5 +443,12 @@ namespace OpenRA.Platforms.Default SDL.SDL_DestroyWindow(window); return true; } + + public void SetScaleModifier(float scale) + { + var oldScaleModifier = scaleModifier; + scaleModifier = scale; + OnWindowScaleChanged(windowScale, windowScale * oldScaleModifier, windowScale, windowScale * scaleModifier); + } } } diff --git a/mods/cnc/chrome/settings.yaml b/mods/cnc/chrome/settings.yaml index 02ab233d12..7ad89a7385 100644 --- a/mods/cnc/chrome/settings.yaml +++ b/mods/cnc/chrome/settings.yaml @@ -140,6 +140,19 @@ Container@SETTINGS_PANEL: Width: 160 Height: 25 Font: Regular + Label@UI_SCALE: + X: 15 + Y: 100 + Width: 120 + Height: 25 + Text: UI Scale: + Align: Right + DropDownButton@UI_SCALE_DROPDOWN: + X: 140 + Y: 100 + Width: 160 + Height: 25 + Font: Regular Label@STATUS_BARS: X: 265 Y: 100 diff --git a/mods/common/chrome/settings.yaml b/mods/common/chrome/settings.yaml index 23aa06dbed..299abb2913 100644 --- a/mods/common/chrome/settings.yaml +++ b/mods/common/chrome/settings.yaml @@ -154,6 +154,19 @@ Background@SETTINGS_PANEL: Width: 160 Height: 25 Font: Regular + Label@UI_SCALE: + X: 15 + Y: 100 + Width: 120 + Height: 25 + Text: UI Scale: + Align: Right + DropDownButton@UI_SCALE_DROPDOWN: + X: 140 + Y: 100 + Width: 160 + Height: 25 + Font: Regular Label@STATUS_BARS: X: 265 Y: 100