acdream/src/AcDream.UI.Abstractions/Panels/Settings/ChatOpacityLink.cs
Erik a819687cf0 feat(chat): Campaign CH slice CH6c — window opacity + transparency setting
Retail's ChatInterface::SetOpacity (0x004F3120) fades the WHOLE composited
window surface with one alpha; UiRenderContext.ApplyAlpha already gated
DrawSprite/DrawRect/DrawFill (since 1da697ec, pre-CH6) but DrawStringDat and
DrawString still passed applyAlpha:false, so text stayed sharp over a
translucent window. Both now route through the same chokepoint.

RetailWindowOpacityController (new) subscribes to a new
RetailWindowManager.WindowRegistered event and drives every registered
window's live Opacity from keyboard-focus state, applied to EVERY window
(chat, floaties, vitals, toolbar, ...) rather than retail's ChatInterface-only
scope — register row AP-190, retiring the stale AP-40 "fixed 0.75, no focus
transition" row in the same commit.

Verified retail's shipped opacity defaults from the decomp (constructor
literals, no cdb needed): the base ChatInterface ctor sets
DefaultOpacity=0.5/ActiveOpacity=1.0, kept unmodified by the four floating
windows; gmMainChatUI's own ctor overrides the main window to 1.0/1.0
(always fully opaque). acdream ships one shared global default (0.5/1.0)
rather than replicating the per-class override — also AP-190. The linking
invariant (raising default above active drags active UP; lowering active
below default drags default DOWN — never a clamp) is ported verbatim as
ChatOpacityLink in AcDream.UI.Abstractions, shared by the live controller
and the new Settings -> Chat tab's two linked opacity sliders.

Persistence: ChatSettings.DefaultOpacity/ActiveOpacity round-trip through
SettingsStore; Save pushes both through IRuntimeSettingsTargets.SetChatOpacity
into the live controller, no restart required.

Rider (CH6a/b re-review): strengthened the grip-media regression guard past
a bare SpriteFile != 0 check — ChatLayoutConformanceTests now drives each
live grip through a real UiRenderContext/TextRenderer (backed by the
in-memory RecordingGpuDevice test double) and asserts the draw call chain
actually queued sprite geometry, via a new TextRenderer.DebugSpriteSegments
test-only accessor.

Full Release suite 12,459 passed / 4 skipped / 0 failed (baseline
12,420/4/0). No subagents, no client launches (session hard constraints);
pending the next connected user gate for visual confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:00:55 +02:00

52 lines
2.6 KiB
C#

namespace AcDream.UI.Abstractions.Panels.Settings;
/// <summary>
/// Retail's linked default/active window-opacity invariant: active &gt;= default,
/// ALWAYS — enforced by dragging the OTHER value, never by clamping the one being
/// set. Verbatim port of <c>ChatInterface::SetDefaultOpacity @0x004F3BC0</c> /
/// <c>SetActiveOpacity @0x004F3C40</c> (docs/research/2026-08-09-chat-retail-window-shell.md
/// §3): raising the default ABOVE the current active value drags active UP to match;
/// lowering the active value BELOW the current default drags default DOWN to match.
/// Retail's options-page sliders share a <c>DualHash</c> link
/// (<c>gmChatOptionsUI::InitOptions @0x0049FC60</c>) — this is why they visibly track
/// each other while dragging.
///
/// <para>
/// Shared by two independent consumers so both apply the identical link math:
/// <c>RetailWindowOpacityController</c> (AcDream.App — the live per-window
/// mechanism, mutating its own two float fields) and <see cref="SettingsPanel"/>'s
/// Chat tab (the draft slider UI, mutating a <see cref="ChatSettings"/> record).
/// Pure functions — no window/render/state dependency — so both layers can call
/// them without violating the App→Abstractions dependency direction.
/// </para>
/// </summary>
public static class ChatOpacityLink
{
/// <summary>
/// Port of <c>ChatInterface::SetDefaultOpacity @0x004F3BC0</c>: set the default
/// (unfocused) opacity to <paramref name="newDefault"/>, dragging
/// <paramref name="currentActive"/> UP if it would otherwise fall below the new
/// default. Never returns an (default, active) pair with active &lt; default.
/// </summary>
public static (float DefaultOpacity, float ActiveOpacity) SetDefault(
float currentActive, float newDefault)
{
newDefault = System.Math.Clamp(newDefault, 0f, 1f);
float active = currentActive < newDefault ? newDefault : currentActive;
return (newDefault, active);
}
/// <summary>
/// Port of <c>ChatInterface::SetActiveOpacity @0x004F3C40</c>: set the active
/// (focused) opacity to <paramref name="newActive"/>, dragging
/// <paramref name="currentDefault"/> DOWN if it would otherwise exceed the new
/// active value. Never returns an (default, active) pair with active &lt; default.
/// </summary>
public static (float DefaultOpacity, float ActiveOpacity) SetActive(
float currentDefault, float newActive)
{
newActive = System.Math.Clamp(newActive, 0f, 1f);
float def = currentDefault > newActive ? newActive : currentDefault;
return (def, newActive);
}
}