fix #379: chat opacity fade was scoped to every window, not just chat

Root cause: RetailWindowOpacityController applied the Default/Active
opacity fade to EVERY registered UiRoot window (vitals, toolbar,
inventory, spellbook, radar, even the Options panel itself), but
retail's ChatInterface::SetDefaultOpacity/SetActiveOpacity are only
ever called by gmMainChatUI/gmFloatyChatUI — the mechanism is chat-only
in retail, not a global window-opacity feature.

Fix: scope the controller's catch-up loop, OnWindowRegistered,
ReapplyAll, and Dispose to WindowNames.Chat/ChatWindow1-4 only; every
other registered window now stays fully opaque regardless of slider
position, matching retail's own scope. Regressed by
RetailWindowOpacityControllerTests.
OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels (registers
vitals/toolbar/chat/a floating chat window and asserts the non-chat
windows never move off 1.0 while chat windows still track Default/
Active correctly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 23:00:57 +02:00
parent c121842664
commit c0b3d8f233
4 changed files with 177 additions and 56 deletions

View file

@ -62,14 +62,36 @@ missing from our template handling.
## #379 — Chat-window opacity applies to ALL retained windows/panels, not only the chat windows ## #379 — Chat-window opacity applies to ALL retained windows/panels, not only the chat windows
**Status:** OPEN — filed 2026-08-11 at Campaign OP gate 4 (user report: **Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's
"When I change the opacity for the chat window only the chatwindows shall re-gate. Filed 2026-08-11 at Campaign OP gate 4 (user report: "When I
change not the other panels"). Retail's ChatInterface::SetOpacity scopes change the opacity for the chat window only the chatwindows shall change
the transparency pair to the chat windows; acdream's not the other panels").
`RetailWindowOpacityController` (CH6c) is applying the pair to other
retained panels too. Determine the exact retail scope (main chat + the **ROOT CAUSE — confirmed structural, not just AP-190's known divergence.**
four floating chat windows only?) from the decomp before narrowing, then Grepped `acclient_2013_pseudo_c.txt` for every call site of
scope the controller's application set to it. `ChatInterface::SetDefaultOpacity`/`SetActiveOpacity`: there are exactly
two, `gmFloatyMainChatUI::UpdateFromPlayerModule`/
`RecvNotice_GameplayOptionChanged` (the main chat window) and
`gmFloatyChatUI::UpdateFromPlayerModule` (the four floating windows),
each calling the method on itself. No other `gmPanelUI` sibling derives
from `ChatInterface`, so no other window class even has these methods in
its vtable — retail's scope is structural, not a runtime choice. **Fix:**
`RetailWindowOpacityController` now scopes `Attach`/`OnWindowRegistered`/
`ReapplyAll`/`Dispose` to exactly the five `ChatWindowNames` (main chat +
`ChatWindow1`-`4`) instead of every window `RetailWindowManager`
registers. Register row AP-190 updated in the same commit (the scope
divergence it recorded is now closed; the default-value/easing/focus-
predicate residuals it also recorded are unaffected). Regressed by
`tests/AcDream.App.Tests/UI/RetailWindowOpacityControllerTests.cs`
(`OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels` pins the exact
applied-window set; the pre-existing tests were updated to register
windows under their real `WindowNames` so the scope check is exercised
by name, matching production).
**Re-gate (§OP5 step 4, rewritten in the gate script): use a chat window
(main or floating) as the "other window," not the toolbar/vitals/another
panel — a non-chat window should now stay fully opaque regardless of the
slider position.**
## #378 — Config-tab dropdown menus render bare (no button well, no arrow) and no popup opens on click ## #378 — Config-tab dropdown menus render bare (no button well, no arrow) and no popup opens on click

View file

@ -427,10 +427,24 @@ chat windows already read when deciding which lines to show.
### Opacity sliders — live drag, linked, never clamping ### Opacity sliders — live drag, linked, never clamping
**Gate-4 re-test note (#379) — step 4 below is REWRITTEN:** the sliders
previously faded EVERY registered window (vitals, toolbar, inventory,
spellbook, radar, the Options panel itself, ...), not just chat windows —
matching retail's own bug-for-bug scope. They now affect ONLY the main
chat window and the four floating chat windows, exactly like retail's
`ChatInterface::SetDefaultOpacity`/`SetActiveOpacity` (which only
`gmMainChatUI`/`gmFloatyChatUI` ever call). **Use a chat window (main or
floating), not the toolbar/vitals/another panel, as your "other window" in
step 4** — a non-chat window's opacity should now stay FIXED (opaque)
regardless of the slider position.
4. **Drag the FIRST slider (Default Opacity) most of the way to the right** 4. **Drag the FIRST slider (Default Opacity) most of the way to the right**
while the Options panel itself is NOT focused/hovered by your mouse (so while the Options panel itself is NOT focused/hovered by your mouse (so
you can see another registered window, e.g. the toolbar or a floating you can see another CHAT window — the main chat window or a floating
chat window, at its UNFOCUSED opacity). Watch that OTHER window's chat window 1-4 — at its UNFOCUSED opacity; a NON-chat window like the
toolbar or vitals bar must stay fully opaque throughout this whole
section, per the #379 note above). Watch that other CHAT window's
transparency change LIVE, continuously, as you drag — not just on transparency change LIVE, continuously, as you drag — not just on
release. This is retail's `SetCurrentValue -> Apply(1)` immediate-apply release. This is retail's `SetCurrentValue -> Apply(1)` immediate-apply
semantic. semantic.

View file

@ -13,14 +13,24 @@ namespace AcDream.App.UI;
/// (<c>docs/research/2026-08-09-chat-retail-window-shell.md</c> §3). /// (<c>docs/research/2026-08-09-chat-retail-window-shell.md</c> §3).
/// ///
/// <para> /// <para>
/// Retail applies this ONLY to <c>ChatInterface</c>-derived windows (the main chat /// <b>#379 fix (2026-08-11, gate 4): scoped to <c>ChatInterface</c>-derived
/// window + the four floaties). acdream applies it to every window /// windows only.</b> Retail's ONLY two callers of
/// <see cref="RetailWindowManager"/> registers — the single Settings transparency /// <c>ChatInterface::SetDefaultOpacity</c>/<c>SetActiveOpacity</c> are
/// slider therefore affects the whole retained UI, not just chat (register row /// <c>gmFloatyMainChatUI::UpdateFromPlayerModule</c>/
/// AP-190). The retail focus test is "does <c>m_chatEntry</c> specifically have /// <c>RecvNotice_GameplayOptionChanged</c> (<c>0x004ce3f0</c>/<c>0x004d25a0</c> —
/// focus"; the generalization here is "does ANY descendant of this window have /// the MAIN chat window) and <c>gmFloatyChatUI::UpdateFromPlayerModule</c>
/// keyboard focus", which <see cref="RetailWindowManager"/> already computes for /// (<c>0x004ce3f0</c> — the four floaty windows), each calling the method on
/// every window via <see cref="RetailWindowHandle.DescendantFocusChanged"/>. /// itself (<c>this</c>). No other <c>gmPanelUI</c> sibling derives from
/// <c>ChatInterface</c>, so no other window class even HAS these methods in its
/// vtable — the scope is structural in retail, not a runtime choice. The
/// PRE-#379 code attached to every window <see cref="RetailWindowManager"/>
/// registers (<see cref="WindowScopeNames"/> below narrows that to the five
/// named chat windows). The retail focus test is "does <c>m_chatEntry</c>
/// specifically have focus"; the generalization here is "does ANY descendant of
/// this window have keyboard focus", which <see cref="RetailWindowManager"/>
/// already computes for every window via
/// <see cref="RetailWindowHandle.DescendantFocusChanged"/> (AP-190 residual,
/// unaffected by this fix).
/// </para> /// </para>
/// ///
/// <para> /// <para>
@ -31,13 +41,13 @@ namespace AcDream.App.UI;
/// <c>ChatInterface::ChatInterface</c> directly with no override, so the four /// <c>ChatInterface::ChatInterface</c> directly with no override, so the four
/// floating windows keep the base 0.5/1.0. <b>CH6c review fix:</b> acdream ships ONE /// floating windows keep the base 0.5/1.0. <b>CH6c review fix:</b> acdream ships ONE
/// shared default — <c>gmMainChatUI</c>'s 1.0/1.0 override, not the base /// shared default — <c>gmMainChatUI</c>'s 1.0/1.0 override, not the base
/// ChatInterface value — applied uniformly to every window including the four /// ChatInterface value — applied to the main chat window AND the four floating
/// floating chat windows. Shipping the base 0.5/1.0 pair globally (the original /// chat windows. Shipping the base 0.5/1.0 pair globally (the original CH6c
/// CH6c behavior) faded the WHOLE registered UI to 50% opacity out of the box, /// behavior) faded the whole registered UI to 50% opacity out of the box; with
/// including windows that can never take keyboard focus and so were stuck at 0.5 /// #379's scoping fix the other eleven <c>gmPanelUI</c> siblings are never
/// permanently; 1.0/1.0 is retail-identical for the 11 non-chat windows and the /// touched by this controller at all (retail-correct — they have no opacity
/// main chat window, and only the four floaties diverge from retail's /// pair to begin with), so only the four floaties diverge from retail's
/// 0.5-while-idle default now — user-settable via the same slider (register row /// 0.5-while-idle default — user-settable via the same slider (register row
/// AP-190). /// AP-190).
/// </para> /// </para>
/// ///
@ -51,6 +61,26 @@ namespace AcDream.App.UI;
/// </summary> /// </summary>
public sealed class RetailWindowOpacityController : IDisposable public sealed class RetailWindowOpacityController : IDisposable
{ {
/// <summary>
/// #379 fix: the exact five <c>ChatInterface</c>-derived windows —
/// <c>gmFloatyMainChatUI</c> (<see cref="WindowNames.Chat"/>) and the four
/// <c>gmFloatyChatUI</c> instances (<see cref="WindowNames.ChatWindow1"/>..
/// <see cref="WindowNames.ChatWindow4"/>) — per this class's own doc
/// comment. Every other registered window name (vitals, toolbar,
/// inventory, spellbook, combat, radar, options, ...) is out of scope by
/// construction: those classes never derive from <c>ChatInterface</c> and
/// so never call <c>SetDefaultOpacity</c>/<c>SetActiveOpacity</c> in
/// retail.
/// </summary>
private static readonly HashSet<string> ChatWindowNames = new(StringComparer.Ordinal)
{
WindowNames.Chat,
WindowNames.ChatWindow1,
WindowNames.ChatWindow2,
WindowNames.ChatWindow3,
WindowNames.ChatWindow4,
};
private readonly RetailWindowManager _manager; private readonly RetailWindowManager _manager;
private readonly HashSet<RetailWindowHandle> _focused = new(); private readonly HashSet<RetailWindowHandle> _focused = new();
private bool _disposed; private bool _disposed;
@ -72,7 +102,8 @@ public sealed class RetailWindowOpacityController : IDisposable
_manager.WindowRegistered += OnWindowRegistered; _manager.WindowRegistered += OnWindowRegistered;
_manager.WindowUnregistered += OnWindowUnregistered; _manager.WindowUnregistered += OnWindowUnregistered;
foreach (RetailWindowHandle handle in _manager.Windows) foreach (RetailWindowHandle handle in _manager.Windows)
Attach(handle); if (ChatWindowNames.Contains(handle.Name))
Attach(handle);
} }
public float DefaultOpacity { get; private set; } public float DefaultOpacity { get; private set; }
@ -118,7 +149,13 @@ public sealed class RetailWindowOpacityController : IDisposable
ReapplyAll(); ReapplyAll();
} }
private void OnWindowRegistered(RetailWindowHandle handle) => Attach(handle); private void OnWindowRegistered(RetailWindowHandle handle)
{
// #379: only the five chat windows are ChatInterface-derived in
// retail — see ChatWindowNames' own doc comment.
if (ChatWindowNames.Contains(handle.Name))
Attach(handle);
}
/// <summary> /// <summary>
/// CH6c review NIT: without this, a window unregistered while it held /// CH6c review NIT: without this, a window unregistered while it held
@ -154,8 +191,12 @@ public sealed class RetailWindowOpacityController : IDisposable
private void ReapplyAll() private void ReapplyAll()
{ {
// #379: scoped to the same five chat windows Attach/OnWindowRegistered
// subscribed — iterating every registered window here (the pre-#379
// shape) is what leaked the slider onto vitals/inventory/spellbook/etc.
foreach (RetailWindowHandle handle in _manager.Windows) foreach (RetailWindowHandle handle in _manager.Windows)
Apply(handle, _focused.Contains(handle)); if (ChatWindowNames.Contains(handle.Name))
Apply(handle, _focused.Contains(handle));
} }
public void Dispose() public void Dispose()
@ -167,7 +208,8 @@ public sealed class RetailWindowOpacityController : IDisposable
_manager.WindowRegistered -= OnWindowRegistered; _manager.WindowRegistered -= OnWindowRegistered;
_manager.WindowUnregistered -= OnWindowUnregistered; _manager.WindowUnregistered -= OnWindowUnregistered;
foreach (RetailWindowHandle handle in _manager.Windows) foreach (RetailWindowHandle handle in _manager.Windows)
handle.DescendantFocusChanged -= OnDescendantFocusChanged; if (ChatWindowNames.Contains(handle.Name))
handle.DescendantFocusChanged -= OnDescendantFocusChanged;
_focused.Clear(); _focused.Clear();
} }
} }

View file

@ -6,8 +6,22 @@ namespace AcDream.App.Tests.UI;
/// Campaign CH slice CH6c: <see cref="RetailWindowOpacityController"/> — the live /// Campaign CH slice CH6c: <see cref="RetailWindowOpacityController"/> — the live
/// per-window opacity mechanism ported from <c>ChatInterface::SetOpacity/ /// per-window opacity mechanism ported from <c>ChatInterface::SetOpacity/
/// SetDefaultOpacity/SetActiveOpacity</c> (<c>0x004F3120</c>/<c>0x004F3BC0</c>/ /// SetDefaultOpacity/SetActiveOpacity</c> (<c>0x004F3120</c>/<c>0x004F3BC0</c>/
/// <c>0x004F3C40</c>), extended to every <see cref="RetailWindowManager"/>-registered /// <c>0x004F3C40</c>).
/// window rather than retail's ChatInterface-only scope (register row AP-190). ///
/// <para>
/// #379 fix (2026-08-11, gate 4): the CH6c-era "extended to every
/// RetailWindowManager-registered window" scope (formerly register row AP-190)
/// was a bug, not a deliberate divergence — retail's ONLY two
/// <c>ChatInterface::SetDefaultOpacity</c>/<c>SetActiveOpacity</c> callers are
/// the main chat window and the four floaty chat windows (see the class's own
/// doc comment). These tests now register windows under their REAL
/// <see cref="WindowNames"/> so the scope check is exercised by name, exactly
/// like the production wiring (<see cref="RetailUiRuntime"/> registers every
/// window under its <see cref="WindowNames"/> constant, never an arbitrary
/// string) — the pre-fix tests used placeholder names ("Vitals", "A", "B",
/// ...) that happened to fall inside the (bugged) unscoped set and would now
/// silently fall outside the real scope for the wrong reason.
/// </para>
/// </summary> /// </summary>
public sealed class RetailWindowOpacityControllerTests public sealed class RetailWindowOpacityControllerTests
{ {
@ -30,7 +44,7 @@ public sealed class RetailWindowOpacityControllerTests
public void Construction_AttachesToAlreadyRegisteredWindows_AtDefaultOpacity() public void Construction_AttachesToAlreadyRegisteredWindows_AtDefaultOpacity()
{ {
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, _) = RegisterWindow(root, "Vitals"); (RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
@ -51,9 +65,9 @@ public sealed class RetailWindowOpacityControllerTests
// The window is mounted AFTER the controller exists — proves the // The window is mounted AFTER the controller exists — proves the
// RetailWindowManager.WindowRegistered subscription (not just the ctor's // RetailWindowManager.WindowRegistered subscription (not just the ctor's
// catch-up loop over already-registered windows) is what applies retail's // catch-up loop over already-registered windows) is what applies the
// GLOBAL opacity scope to every future Mount* call too. // chat-scoped opacity to every future Mount* call too.
(RetailWindowHandle handle, _) = RegisterWindow(root, "Toolbar"); (RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.ChatWindow1);
Assert.Equal(0.3f, handle.Opacity); Assert.Equal(0.3f, handle.Opacity);
} }
@ -62,7 +76,7 @@ public sealed class RetailWindowOpacityControllerTests
public void FocusEnteringAWindow_SwitchesToActiveOpacity_LeavingSwitchesBack() public void FocusEnteringAWindow_SwitchesToActiveOpacity_LeavingSwitchesBack()
{ {
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
Assert.Equal(0.5f, handle.Opacity); Assert.Equal(0.5f, handle.Opacity);
@ -75,26 +89,55 @@ public sealed class RetailWindowOpacityControllerTests
GC.KeepAlive(controller); GC.KeepAlive(controller);
} }
/// <summary>
/// #379 regression: pins the EXACT applied-window set to the five real
/// <see cref="WindowNames"/> chat windows. Before the fix, this same setup
/// left <c>vitals</c>/<c>toolbar</c> at 0.4 (the DefaultOpacity) instead of
/// their own authored 1.0 — the user's literal complaint ("only the
/// chatwindows shall change not the other panels").
/// </summary>
[Fact] [Fact]
public void OpacityFade_AppliesToEveryRegisteredWindow_NotJustChat() public void OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels()
{ {
// 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(); UiRoot root = NewRoot();
(RetailWindowHandle vitals, _) = RegisterWindow(root, "Vitals"); (RetailWindowHandle vitals, _) = RegisterWindow(root, WindowNames.Vitals);
(RetailWindowHandle toolbar, UiElement toolbarChild) = RegisterWindow(root, "Toolbar"); (RetailWindowHandle toolbar, UiElement toolbarChild) = RegisterWindow(root, WindowNames.Toolbar);
(RetailWindowHandle chat, _) = RegisterWindow(root, WindowNames.Chat);
(RetailWindowHandle floaty1, UiElement floaty1Child) = RegisterWindow(root, WindowNames.ChatWindow1);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.4f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.4f, activeOpacity: 1.0f);
Assert.Equal(0.4f, vitals.Opacity); // Non-chat windows never had Apply called on them at all — they sit at
Assert.Equal(0.4f, toolbar.Opacity); // UiElement.Opacity's own 1.0 ctor default, untouched by the slider.
Assert.Equal(1.0f, vitals.Opacity);
Assert.Equal(1.0f, toolbar.Opacity);
// The two chat windows DID get the DefaultOpacity applied.
Assert.Equal(0.4f, chat.Opacity);
Assert.Equal(0.4f, floaty1.Opacity);
root.SetKeyboardFocus(toolbarChild); root.SetKeyboardFocus(toolbarChild);
Assert.Equal(0.4f, vitals.Opacity); // unrelated window: still unfocused // Focusing a NON-chat window's descendant still does not touch its own
Assert.Equal(1.0f, toolbar.Opacity); // the focused one: active // opacity (that window was never subscribed to focus changes either)
// NOR does it bleed the ACTIVE opacity onto the chat windows — they
// stay at DefaultOpacity since neither of THEM has focus.
Assert.Equal(1.0f, vitals.Opacity);
Assert.Equal(1.0f, toolbar.Opacity);
Assert.Equal(0.4f, chat.Opacity);
Assert.Equal(0.4f, floaty1.Opacity);
root.SetKeyboardFocus(floaty1Child);
controller.SetActiveOpacity(0.6f);
// A live re-apply (SetActiveOpacity -> ReapplyAll) is ALSO scoped — the
// two non-chat windows remain untouched throughout, proving ReapplyAll
// itself (not just the initial Attach) respects the scope. The FOCUSED
// chat window picks up the new active value; the unfocused one stays
// at the (unchanged) default.
Assert.Equal(1.0f, vitals.Opacity);
Assert.Equal(1.0f, toolbar.Opacity);
Assert.Equal(0.4f, chat.Opacity);
Assert.Equal(0.6f, floaty1.Opacity);
} }
[Fact] [Fact]
@ -104,8 +147,8 @@ public sealed class RetailWindowOpacityControllerTests
// raising DEFAULT above the current ACTIVE value drags active UP to // raising DEFAULT above the current ACTIVE value drags active UP to
// match — it never clamps the default down instead. // match — it never clamps the default down instead.
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle unfocused, _) = RegisterWindow(root, "A"); (RetailWindowHandle unfocused, _) = RegisterWindow(root, WindowNames.Chat);
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B"); (RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, WindowNames.ChatWindow1);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.3f, activeOpacity: 0.5f); root.WindowManager, defaultOpacity: 0.3f, activeOpacity: 0.5f);
root.SetKeyboardFocus(focusedChild); root.SetKeyboardFocus(focusedChild);
@ -125,8 +168,8 @@ public sealed class RetailWindowOpacityControllerTests
{ {
// Symmetric case (ChatInterface::SetActiveOpacity @0x004F3C40). // Symmetric case (ChatInterface::SetActiveOpacity @0x004F3C40).
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle unfocused, _) = RegisterWindow(root, "A"); (RetailWindowHandle unfocused, _) = RegisterWindow(root, WindowNames.Chat);
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B"); (RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, WindowNames.ChatWindow1);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 0.7f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 0.7f);
root.SetKeyboardFocus(focusedChild); root.SetKeyboardFocus(focusedChild);
@ -145,7 +188,7 @@ public sealed class RetailWindowOpacityControllerTests
// UpdateFromPlayerModule (0x004CE3F0) reads/applies Default first, then // UpdateFromPlayerModule (0x004CE3F0) reads/applies Default first, then
// Active — the shape used to push a freshly loaded ChatSettings pair. // Active — the shape used to push a freshly loaded ChatSettings pair.
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, _) = RegisterWindow(root, "Chat"); (RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
@ -174,7 +217,7 @@ public sealed class RetailWindowOpacityControllerTests
public void Dispose_UnsubscribesFromFocusChanges() public void Dispose_UnsubscribesFromFocusChanges()
{ {
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
@ -198,14 +241,14 @@ public sealed class RetailWindowOpacityControllerTests
// kind a lingering external reference to the handle could still fire) // kind a lingering external reference to the handle could still fire)
// must not reach the controller anymore. // must not reach the controller anymore.
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
root.SetKeyboardFocus(child); root.SetKeyboardFocus(child);
Assert.Equal(1.0f, handle.Opacity); Assert.Equal(1.0f, handle.Opacity);
root.WindowManager.Unregister("Chat"); root.WindowManager.Unregister(WindowNames.Chat);
// Unregister hides the outer frame, which drops keyboard focus off // Unregister hides the outer frame, which drops keyboard focus off
// the now-invisible child — the manager's (still-live at that point) // the now-invisible child — the manager's (still-live at that point)
@ -228,7 +271,7 @@ public sealed class RetailWindowOpacityControllerTests
public void SetMutators_AfterDispose_AreNoOps() public void SetMutators_AfterDispose_AreNoOps()
{ {
UiRoot root = NewRoot(); UiRoot root = NewRoot();
(RetailWindowHandle handle, _) = RegisterWindow(root, "Chat"); (RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
var controller = new RetailWindowOpacityController( var controller = new RetailWindowOpacityController(
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);