diff --git a/src/AcDream.App/UI/Layout/ProjectileDebugOverlayController.cs b/src/AcDream.App/UI/Layout/ProjectileDebugOverlayController.cs index 859f3d615..da6d09799 100644 --- a/src/AcDream.App/UI/Layout/ProjectileDebugOverlayController.cs +++ b/src/AcDream.App/UI/Layout/ProjectileDebugOverlayController.cs @@ -9,7 +9,6 @@ internal sealed class ProjectileDebugOverlayController private static readonly Vector4 ClearColor = new(0f, 1f, 0f, 0.95f); private static readonly Vector4 BlockedColor = new(1f, 0f, 0f, 0.95f); - private readonly UiOverlayHost _host; private readonly UiPanel _root; private readonly Func> _samples; private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> @@ -17,12 +16,10 @@ internal sealed class ProjectileDebugOverlayController private readonly List _markers = []; private ProjectileDebugOverlayController( - UiOverlayHost host, UiPanel root, Func> samples, Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> camera) { - _host = host; _root = root; _samples = samples; _camera = camera; @@ -42,7 +39,7 @@ internal static ProjectileDebugOverlayController Mount( ArgumentNullException.ThrowIfNull(samples); ArgumentNullException.ThrowIfNull(camera); UiPanel root = host.AddLayer("PluginProjectileDebugOverlay"); - return new ProjectileDebugOverlayController(host, root, samples, camera); + return new ProjectileDebugOverlayController(root, samples, camera); } internal void Tick() @@ -58,7 +55,6 @@ internal void Tick() } EnsureMarkerCount(samples.Count); - _host.SetViewport(camera.Viewport); int visible = 0; for (int index = 0; index < samples.Count; index++) { diff --git a/src/AcDream.App/UI/Layout/UiOverlayHost.cs b/src/AcDream.App/UI/Layout/UiOverlayHost.cs index 6393477fe..431ae6749 100644 --- a/src/AcDream.App/UI/Layout/UiOverlayHost.cs +++ b/src/AcDream.App/UI/Layout/UiOverlayHost.cs @@ -114,17 +114,20 @@ private static void MakeSubtreeClickThrough(UiElement element) /// Children are clipped to their parent's rectangle by default, so an /// overlay whose rectangle is still zero-sized shows nothing at all. That is /// why the host seeds its rectangle from the interface root at mount and -/// re-applies it to the root and every layer on -/// . +/// re-applies it to the root and every layer on . +/// The interface root is the one source of that rectangle: a world overlay +/// projects into its camera's viewport but never resizes the layers. /// internal sealed class UiOverlayHost { + private readonly UiRoot _host; private readonly UiOverlayLayer _root; private readonly List _layers = []; private Vector2 _viewport; - private UiOverlayHost(UiOverlayLayer root, Vector2 viewport) + private UiOverlayHost(UiRoot host, UiOverlayLayer root, Vector2 viewport) { + _host = host; _root = root; _viewport = viewport; } @@ -144,7 +147,7 @@ internal static UiOverlayHost Mount(UiRoot host) ZOrder = UiOverlayZOrder.SharedHostRoot, }; host.AddChild(root); - var overlayHost = new UiOverlayHost(root, new Vector2(host.Width, host.Height)); + var overlayHost = new UiOverlayHost(host, root, new Vector2(host.Width, host.Height)); overlayHost.ApplyViewport(); return overlayHost; } @@ -166,14 +169,17 @@ internal UiOverlayLayer AddLayer(string name) } /// - /// Points the host at the rectangle the world is being drawn into. Every - /// layer covers the same rectangle: an overlay is by definition the whole - /// screen, and one shared value keeps two owners from clipping each other. + /// Brings the root and every layer to the interface root's current size. + /// The interface calls it every frame, after sizing its root to the + /// screen: the root can also be resized elsewhere (restoring the layout + /// on entering the world does), so the host follows the root rather than + /// waiting to be told of a change. /// - internal void SetViewport(Vector2 viewport) + internal void FollowRoot() { - if (_viewport == viewport) return; - _viewport = viewport; + var size = new Vector2(_host.Width, _host.Height); + if (_viewport == size) return; + _viewport = size; ApplyViewport(); } diff --git a/src/AcDream.App/UI/Layout/WorldLabelOverlayController.cs b/src/AcDream.App/UI/Layout/WorldLabelOverlayController.cs index 48d093078..f1d4a333a 100644 --- a/src/AcDream.App/UI/Layout/WorldLabelOverlayController.cs +++ b/src/AcDream.App/UI/Layout/WorldLabelOverlayController.cs @@ -223,7 +223,6 @@ protected override void OnDraw(UiRenderContext ctx) /// internal sealed class WorldLabelOverlayController { - private readonly UiOverlayHost _host; private readonly UiOverlayLayer _layer; private readonly WorldLabelLayerElement _element; private readonly UiDatFont _font; @@ -234,7 +233,6 @@ internal sealed class WorldLabelOverlayController private List _back = []; private WorldLabelOverlayController( - UiOverlayHost host, UiOverlayLayer layer, WorldLabelLayerElement element, UiDatFont font, @@ -242,7 +240,6 @@ private WorldLabelOverlayController( Func anchor, Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> camera) { - _host = host; _layer = layer; _element = element; _font = font; @@ -277,7 +274,7 @@ internal static WorldLabelOverlayController Mount( }; layer.AddChild(element); return new WorldLabelOverlayController( - host, layer, element, font, labels, anchor, camera); + layer, element, font, labels, anchor, camera); } internal void Tick() @@ -293,7 +290,6 @@ internal void Tick() return; } - _host.SetViewport(camera.Viewport); _element.Width = camera.Viewport.X; _element.Height = camera.Viewport.Y; WorldLabelLayout.Place( diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 5f9abbd68..bd12a537e 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -777,10 +777,12 @@ public void Draw(System.Numerics.Vector2 screenSize) _persistence?.SetGameplayActive(gameplay); if (gameplay) _persistence?.ReflowToScreen(); _lastScreenSize = screenSize; - // The overlay band covers the whole screen; a canvas anchored to - // a right or bottom edge is placed against this rectangle. - _overlayHost?.SetViewport(screenSize); } + // The overlay band covers the whole screen; a canvas anchored to a + // right or bottom edge is placed against this rectangle. Followed + // every frame, not only on a change seen above: restoring the layout + // resizes the root without passing through here. + _overlayHost?.FollowRoot(); Host.Draw(screenSize); } diff --git a/tests/AcDream.App.Tests/UI/Layout/PluginCanvasElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/PluginCanvasElementTests.cs index da9371b41..68675a462 100644 --- a/tests/AcDream.App.Tests/UI/Layout/PluginCanvasElementTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/PluginCanvasElementTests.cs @@ -465,7 +465,9 @@ public void ACanvasIsPlacedByAnchorPlusOffsetAndFollowsTheViewport() registration.Anchor = PluginCanvasAnchor.BottomRight; registration.Offset = new PluginPoint(-10, -10); - harness.Host.SetViewport(new Vector2(1024f, 768f)); + harness.Root.Width = 1024f; + harness.Root.Height = 768f; + harness.Host.FollowRoot(); harness.Frame(); Assert.Equal((814f, 658f), (element.Left, element.Top)); } diff --git a/tests/AcDream.App.Tests/UI/Layout/UiOverlayHostTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiOverlayHostTests.cs index 9bb00951b..64ca0a963 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiOverlayHostTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiOverlayHostTests.cs @@ -1,7 +1,9 @@ +using System.Collections.Generic; using System.Linq; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; +using AcDream.Plugin.Abstractions; namespace AcDream.App.Tests.UI.Layout; @@ -151,14 +153,20 @@ public void TheRootTakesItsRectangleFromTheInterfaceRootAtMount() } [Fact] - public void ANewViewportReachesTheRootAndEveryLayerIncludingOnesAddedLater() + public void ANewRootSizeReachesTheRootAndEveryLayerIncludingOnesAddedLater() { - UiOverlayHost host = UiOverlayHost.Mount(Root()); + UiRoot interfaceRoot = Root(); + UiOverlayHost host = UiOverlayHost.Mount(interfaceRoot); UiOverlayLayer first = host.AddLayer("First"); - host.SetViewport(new Vector2(1920f, 1080f)); + // Resized behind the host's back, the way restoring the layout on + // entering the world resizes it: the host is never told the size. + interfaceRoot.Width = 1920f; + interfaceRoot.Height = 1080f; + host.FollowRoot(); UiOverlayLayer second = host.AddLayer("Second"); + Assert.Equal(new Vector2(1920f, 1080f), host.Viewport); foreach (UiElement element in new UiElement[] { host.Root, first, second }) { Assert.Equal(0f, element.Left); @@ -168,6 +176,32 @@ public void ANewViewportReachesTheRootAndEveryLayerIncludingOnesAddedLater() } } + [Fact] + public void AWorldOverlayDrawingIntoADifferentViewportLeavesTheLayersAtTheScreenSize() + { + var interfaceRoot = new UiRoot { Width = 1920f, Height = 1080f }; + UiOverlayHost host = UiOverlayHost.Mount(interfaceRoot); + UiOverlayLayer canvases = host.AddLayer("Canvases"); + var samples = new List + { + new(new Vector3(0f, 0f, -10f), true, 0.4f), + }; + ProjectileDebugOverlayController overlay = ProjectileDebugOverlayController.Mount( + host, + () => samples, + () => (Matrix4x4.Identity, + Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 2f, 4f / 3f, 0.1f, 100f), + new Vector2(800f, 600f))); + + overlay.Tick(); + + foreach (UiElement element in new UiElement[] { host.Root, canvases }) + { + Assert.Equal(1920f, element.Width); + Assert.Equal(1080f, element.Height); + } + } + [Fact] public void ALayerStartsHiddenClickThroughAnchorlessAndWithoutChrome() {