using AcDream.App.UI; namespace AcDream.App.Tests.UI; /// /// Campaign CH slice CH6c: — the live /// per-window opacity mechanism ported from ChatInterface::SetOpacity/ /// SetDefaultOpacity/SetActiveOpacity (0x004F3120/0x004F3BC0/ /// 0x004F3C40), extended to every -registered /// window rather than retail's ChatInterface-only scope (register row AP-190). /// public sealed class RetailWindowOpacityControllerTests { private static UiRoot NewRoot() => new() { Width = 800f, Height = 600f }; /// Registers a bare window ("outer frame" + one focusable child) and /// returns both — mirrors how a real window has a content descendant the /// chat entry / any input field could take focus on. private static (RetailWindowHandle handle, UiElement child) RegisterWindow(UiRoot root, string name) { var frame = new UiPanel { Width = 100f, Height = 100f }; var child = new UiPanel { Width = 10f, Height = 10f, AcceptsFocus = true }; frame.AddChild(child); root.AddChild(frame); RetailWindowHandle handle = root.WindowManager.Register(name, frame); return (handle, child); } [Fact] public void Construction_AttachesToAlreadyRegisteredWindows_AtDefaultOpacity() { UiRoot root = NewRoot(); (RetailWindowHandle handle, _) = RegisterWindow(root, "Vitals"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); // No descendant has focus yet — every window starts at the DEFAULT // (unfocused) opacity, matching retail's unfocused ChatInterface state. Assert.Equal(0.5f, handle.Opacity); Assert.Equal(0.5f, controller.DefaultOpacity); Assert.Equal(1.0f, controller.ActiveOpacity); } [Fact] public void WindowRegisteredAfterConstruction_PicksUpLiveOpacityImmediately() { UiRoot root = NewRoot(); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.3f, activeOpacity: 0.9f); // The window is mounted AFTER the controller exists — proves the // RetailWindowManager.WindowRegistered subscription (not just the ctor's // catch-up loop over already-registered windows) is what applies retail's // GLOBAL opacity scope to every future Mount* call too. (RetailWindowHandle handle, _) = RegisterWindow(root, "Toolbar"); Assert.Equal(0.3f, handle.Opacity); } [Fact] public void FocusEnteringAWindow_SwitchesToActiveOpacity_LeavingSwitchesBack() { UiRoot root = NewRoot(); (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); Assert.Equal(0.5f, handle.Opacity); root.SetKeyboardFocus(child); Assert.Equal(1.0f, handle.Opacity); root.SetKeyboardFocus(null); Assert.Equal(0.5f, handle.Opacity); GC.KeepAlive(controller); } [Fact] public void OpacityFade_AppliesToEveryRegisteredWindow_NotJustChat() { // The CH6c scope extension: retail's ChatInterface::SetOpacity only ever // runs on chat-derived windows. acdream applies the SAME mechanism to // every RetailWindowManager window — vitals, toolbar, whatever else is // mounted — matching the task's GLOBAL-option framing. UiRoot root = NewRoot(); (RetailWindowHandle vitals, _) = RegisterWindow(root, "Vitals"); (RetailWindowHandle toolbar, UiElement toolbarChild) = RegisterWindow(root, "Toolbar"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.4f, activeOpacity: 1.0f); Assert.Equal(0.4f, vitals.Opacity); Assert.Equal(0.4f, toolbar.Opacity); root.SetKeyboardFocus(toolbarChild); Assert.Equal(0.4f, vitals.Opacity); // unrelated window: still unfocused Assert.Equal(1.0f, toolbar.Opacity); // the focused one: active } [Fact] public void SetDefaultOpacity_AboveCurrentActive_DragsActiveUp_AndReappliesEverywhere() { // Decomp-verified linking (ChatInterface::SetDefaultOpacity @0x004F3BC0): // raising DEFAULT above the current ACTIVE value drags active UP to // match — it never clamps the default down instead. UiRoot root = NewRoot(); (RetailWindowHandle unfocused, _) = RegisterWindow(root, "A"); (RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.3f, activeOpacity: 0.5f); root.SetKeyboardFocus(focusedChild); Assert.Equal(0.3f, unfocused.Opacity); Assert.Equal(0.5f, focused.Opacity); controller.SetDefaultOpacity(0.9f); Assert.Equal(0.9f, controller.DefaultOpacity); Assert.Equal(0.9f, controller.ActiveOpacity); Assert.Equal(0.9f, unfocused.Opacity); Assert.Equal(0.9f, focused.Opacity); } [Fact] public void SetActiveOpacity_BelowCurrentDefault_DragsDefaultDown_AndReappliesEverywhere() { // Symmetric case (ChatInterface::SetActiveOpacity @0x004F3C40). UiRoot root = NewRoot(); (RetailWindowHandle unfocused, _) = RegisterWindow(root, "A"); (RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 0.7f); root.SetKeyboardFocus(focusedChild); controller.SetActiveOpacity(0.1f); Assert.Equal(0.1f, controller.DefaultOpacity); Assert.Equal(0.1f, controller.ActiveOpacity); Assert.Equal(0.1f, unfocused.Opacity); Assert.Equal(0.1f, focused.Opacity); } [Fact] public void SetOpacity_AppliesBothInRetailsUpdateFromPlayerModuleOrder() { // UpdateFromPlayerModule (0x004CE3F0) reads/applies Default first, then // Active — the shape used to push a freshly loaded ChatSettings pair. UiRoot root = NewRoot(); (RetailWindowHandle handle, _) = RegisterWindow(root, "Chat"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); controller.SetOpacity(defaultOpacity: 0.2f, activeOpacity: 0.6f); Assert.Equal(0.2f, controller.DefaultOpacity); Assert.Equal(0.6f, controller.ActiveOpacity); Assert.Equal(0.2f, handle.Opacity); } [Fact] public void ConstructorSeed_EnforcesTheActiveGreaterThanOrEqualDefaultInvariant() { // A corrupt/hand-edited settings.json could carry active < default. // The seed collapses through the SAME link the live setters use. UiRoot root = NewRoot(); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.8f, activeOpacity: 0.2f); Assert.True(controller.ActiveOpacity >= controller.DefaultOpacity); Assert.Equal(0.2f, controller.DefaultOpacity); Assert.Equal(0.2f, controller.ActiveOpacity); } [Fact] public void Dispose_UnsubscribesFromFocusChanges() { UiRoot root = NewRoot(); (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); var controller = new RetailWindowOpacityController( root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); controller.Dispose(); root.SetKeyboardFocus(child); // Still at the last value the controller applied before disposal — a // focus change after Dispose is not observed anymore. Assert.Equal(0.5f, handle.Opacity); } }