diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index 2a636103..a037ed75 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -27,10 +27,12 @@ namespace AcDream.App.UI.Layout;
///
///
///
-/// The expand-detail overlay present in the front container carries ONLY named
-/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
-/// TryGetValue("") filter in excludes it
-/// automatically.
+/// The expand-detail icon overlay present in EACH container (back + front)
+/// carries ONLY named states ("HideDetail"/"ShowDetail") — no "" DirectState
+/// entry — so the TryGetValue("") filter in
+/// excludes it from slice extraction; then absorbs
+/// it into for the retail
+/// HideDetail/ShowDetail click toggle.
///
///
public static class DatWidgetFactory
@@ -76,6 +78,15 @@ public static class DatWidgetFactory
UiElement e = info.Type switch
{
UiRadar.RetailClassId => new UiRadar(), // gmRadarUI (Register 0x004D8B80)
+ // The vitals window roots — gmVitalsUI (0x10000009 @0x004BFE10),
+ // gmFloatyVitalsUI (0x1000004D, stacked 0x2100006C), and
+ // gmFloatySideVitalsUI (0x10000056, side-by-side 0x21000075). All
+ // three share the inherited HideDetail/ShowDetail press toggle
+ // (gmVitalsUI::ListenToElementMessage @0x004BFC00) — see UiVitalsRoot.
+ UiVitalsRoot.GmVitalsClassId
+ or UiVitalsRoot.GmFloatyVitalsClassId
+ or UiVitalsRoot.GmFloatySideVitalsClassId
+ => new UiVitalsRoot(info, resolve),
1 => BuildButton(info, resolve, elementFont, fontResolve, stringResolve), // UIElement_Button
2 => new UiDatElement(info, resolve) // UIElement_Dragbar (Register @ 0x0046C840)
{
@@ -446,8 +457,11 @@ public static class DatWidgetFactory
/// │ ├── left-cap image (→ front-left sprite)
/// │ ├── center image (→ front-tile sprite)
/// │ ├── right-cap image (→ front-right sprite)
- /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
- /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
+ /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState —
+ /// │ absorbed as the bright fill-clipped detail icon; the back
+ /// │ container has a matching dim one — see ConfigureDetailOverlay)
+ /// └── text label (Type 0) (built as a real UiText child by LayoutImporter's meter
+ /// carve-out; Fill/Label providers bound by VitalsController)
///
///
///
@@ -508,6 +522,35 @@ public static class DatWidgetFactory
m.FrontLeft = fl;
m.FrontTile = ft;
m.FrontRight = fr;
+
+ // The expand-detail icon overlays (the 0x100004A9 child of EACH
+ // container — dim icon in the back track, bright icon in the
+ // fill-clipped front layer; health heart 0x06007490/91, stamina
+ // sword 0x06007492/93, mana scepter 0x06007494/95). They author
+ // media only for the named ShowDetail state (HideDetail authors
+ // File=0), which is why SliceIds' DirectState filter already
+ // excludes them from the slice extraction above. Absorb them into
+ // the meter alongside the slices so the HideDetail/ShowDetail
+ // click toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00)
+ // can draw retail's graphical vitals mode. PassToChildren on the
+ // meter's own media-less HideDetail/ShowDetail StateDescs gates
+ // the cascade to the absorbed number label
+ // (UIElement::SetState @0x00464E70).
+ ElementInfo? backOverlay = DetailOverlay(containers[0]);
+ ElementInfo? frontOverlay = DetailOverlay(containers[1]);
+ if (backOverlay is not null || frontOverlay is not null)
+ {
+ bool passToChildren = info.States.Values.Any(
+ static s => s.Name is "HideDetail" or "ShowDetail" && s.PassToChildren);
+ m.ConfigureDetailOverlay(
+ backOverlay is not null ? backOverlay.StateMedia["ShowDetail"].File : 0u,
+ backOverlay?.X ?? 0f, backOverlay?.Y ?? 0f,
+ backOverlay?.Width ?? 0f, backOverlay?.Height ?? 0f,
+ frontOverlay is not null ? frontOverlay.StateMedia["ShowDetail"].File : 0u,
+ frontOverlay?.X ?? 0f, frontOverlay?.Y ?? 0f,
+ frontOverlay?.Width ?? 0f, frontOverlay?.Height ?? 0f,
+ passToChildren);
+ }
}
else if (containers.Count == 1 && containers[0].StateMedia.ContainsKey(""))
{
@@ -617,6 +660,18 @@ public static class DatWidgetFactory
=> container.Children.Count(c =>
c.StateMedia.TryGetValue("", out var media) && media.File != 0) >= 3;
+ ///
+ /// Finds a container's expand-detail icon overlay: the child that authors a
+ /// non-zero image for the named ShowDetail state and NO DirectState image
+ /// (the 0x100004A9 shape in both vitals layouts 0x2100006C / 0x21000075).
+ /// Returns null when the container has none (non-vitals meters).
+ ///
+ private static ElementInfo? DetailOverlay(ElementInfo container)
+ => container.Children.FirstOrDefault(static c =>
+ !c.StateMedia.ContainsKey("")
+ && c.StateMedia.TryGetValue("ShowDetail", out var media)
+ && media.File != 0);
+
private static bool HasStatefulFill(ElementInfo container)
=> container.States.Any(pair =>
pair.Key != UiStateInfo.DirectStateId
diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs
index 32cbaa16..6bc56c04 100644
--- a/src/AcDream.App/UI/Layout/LayoutImporter.cs
+++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs
@@ -181,11 +181,15 @@ public static class LayoutImporter
// widgets via FindElement and bind LinesProvider without injecting new runtime nodes.
//
// Type-3 children are SKIPPED here because BuildMeter already consumed them (they
- // carry the 3-slice sprite ids, not text content; building them again would
- // double-draw the bar art). All other child types are built normally.
+ // carry the 3-slice sprite ids + the ShowDetail icon overlays, not text content;
+ // building them again would double-draw the bar art). All other child types are
+ // built normally.
//
- // Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children
- // (no text children). This loop finds nothing for them → no change to vitals.
+ // For vitals this loop builds exactly the cur/max number label (0x100000EB/ED/EF,
+ // Type 0 → merged Type 12 → UiText): VitalsController binds its LinesProvider, and
+ // the HideDetail/ShowDetail cascade from the window root reaches it THROUGH the
+ // meter (UiMeter.TrySetRetailState forwards to stateful children), flipping its
+ // per-state 0x3B visibility exactly like retail's PassToChildren SetState walk.
foreach (var child in info.Children)
{
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
diff --git a/src/AcDream.App/UI/Layout/UiVitalsRoot.cs b/src/AcDream.App/UI/Layout/UiVitalsRoot.cs
new file mode 100644
index 00000000..429a0560
--- /dev/null
+++ b/src/AcDream.App/UI/Layout/UiVitalsRoot.cs
@@ -0,0 +1,95 @@
+using System;
+
+namespace AcDream.App.UI.Layout;
+
+///
+/// Behavioral root widget for the vitals windows — retail's gmVitalsUI
+/// family (gmVitalsUI 0x10000009, gmFloatyVitalsUI 0x1000004D =
+/// the stacked window LayoutDesc 0x2100006C, gmFloatySideVitalsUI
+/// 0x10000056 = the side-by-side window LayoutDesc 0x21000075; registrations
+/// @0x004BFE10 / @0x004CED90 / @0x004D0490).
+///
+///
+/// The click toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00,
+/// inherited verbatim by both floaty subclasses): on Element_mouse_press
+/// (0x1C) with dwParam1 7 (left) or 0xA (right) the root flips
+/// SetState(m_state == HideDetail ? ShowDetail : HideDetail).
+/// State semantics from the authored data + UIElement::OnSetAttribute
+/// case 8 (0x3B = invisible, SetVisible(value == 0) @0x00462DAE):
+///
+///
+/// - Undef (login default; DefaultState is authored
+/// Undef and nothing calls SetState at init): numbers visible, no icons —
+/// visually identical to HideDetail.
+/// - HideDetail (0x10000006): the numeric mode — the
+/// cur/max labels author {0x3B:false} (visible); the icon overlays author
+/// File=0 (nothing).
+/// - ShowDetail (0x10000007): the graphical mode —
+/// labels author {0x3B:true} (hidden); each bar shows its authored icon pair
+/// (dim back + bright fill-clipped front: heart / sword / scepter).
+///
+///
+///
+/// The first press from Undef lands on HideDetail (retail's exact expression:
+/// ecx = (m_state == 0x10000006); SetState(ecx + 0x10000006) — any
+/// state that is not HideDetail, including the initial Undef, goes to
+/// HideDetail first), so the first click appears to do nothing and the second
+/// enters icon mode. NOT persisted anywhere: gmGamePlayUI::SaveScreenLayout
+/// @0x004EAD50 writes only window rects, and no PlayerModule option is
+/// touched — the mode resets to Undef every session, per window.
+///
+///
+///
+/// Press routing: retail broadcasts the press to the pressed element
+/// and forwards up the parent chain (UIElement::ListenToElementMessage
+/// @0x00462340 → ForwardElementMessage), but UIElement_Dragbar
+/// (@0x0046C850) and UIElement_Resizebar (@0x0046B930) both return 2
+/// (consumed) unconditionally — a press that starts a window move or resize
+/// never reaches the vitals root. Mirrored here: presses whose hit target is a
+/// move handle or resize grip are ignored. The handler returns false so the
+/// event keeps bubbling, matching retail's fall-through to the base handler.
+///
+///
+public sealed class UiVitalsRoot : UiDatElement
+{
+ /// gmVitalsUI registered element class (@0x004BFE1A).
+ public const uint GmVitalsClassId = 0x10000009u;
+ /// gmFloatyVitalsUI registered element class (@0x004CED9A) — the stacked window root.
+ public const uint GmFloatyVitalsClassId = 0x1000004Du;
+ /// gmFloatySideVitalsUI registered element class (@0x004D049A) — the side-by-side window root.
+ public const uint GmFloatySideVitalsClassId = 0x10000056u;
+
+ public UiVitalsRoot(ElementInfo info, Func resolve)
+ : base(info, resolve)
+ {
+ }
+
+ public override bool OnEvent(in UiEvent e)
+ {
+ if (e.Type is UiEventType.MouseDown or UiEventType.RightDown
+ && !PressConsumedByChrome(e.Target))
+ {
+ // gmVitalsUI::ListenToElementMessage @0x004BFC04:
+ // this->SetState(m_state == HideDetail ? ShowDetail : HideDetail)
+ // then falls through to the base handler (keep bubbling → false).
+ TrySetRetailState(
+ ActiveRetailStateId == RetailUiStateIds.HideDetail
+ ? RetailUiStateIds.ShowDetail
+ : RetailUiStateIds.HideDetail);
+ }
+ return false;
+ }
+
+ ///
+ /// True when the pressed element is (or sits inside) a window-move handle
+ /// or resize grip — the two retail element classes that consume the press
+ /// before it can bubble to the vitals root.
+ ///
+ private bool PressConsumedByChrome(UiElement? target)
+ {
+ for (UiElement? w = target; w is not null && w != this; w = w.Parent)
+ if (w.WindowMoveHandle || w is UiResizeGrip)
+ return true;
+ return false;
+ }
+}
diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs
index db74eaaf..6a90d87e 100644
--- a/src/AcDream.App/UI/UiMeter.cs
+++ b/src/AcDream.App/UI/UiMeter.cs
@@ -25,6 +25,23 @@ public sealed class UiMeter : UiElement, IUiDatStateful
private readonly Dictionary _stateLabels = new();
private (string Text, UiMeterLabelAlign Align)? _activeStateLabel;
+ // Vitals ShowDetail icon overlays (see ConfigureDetailOverlay).
+ private bool _detailConfigured;
+ private bool _detailPassToChildren;
+ private uint _detailBackSprite;
+ private uint _detailFrontSprite;
+ private (float X, float Y, float W, float H) _detailBackRect;
+ private (float X, float Y, float W, float H) _detailFrontRect;
+
+ /// True when this meter absorbed the vitals detail-icon overlays. Exposed for tests.
+ internal bool HasDetailOverlay => _detailConfigured;
+ /// The dim back-container detail icon (ShowDetail media). Exposed for tests.
+ internal uint DetailBackSprite => _detailBackSprite;
+ /// The bright fill-clipped front-container detail icon. Exposed for tests.
+ internal uint DetailFrontSprite => _detailFrontSprite;
+ /// The back overlay's authored meter-local rect. Exposed for tests.
+ internal (float X, float Y, float W, float H) DetailBackRect => _detailBackRect;
+
/// Dat element id, set by the layout importer so duplicated page copies can be scoped.
public uint ElementId { get; set; }
@@ -114,8 +131,54 @@ public sealed class UiMeter : UiElement, IUiDatStateful
_stateLabels[stateId] = (text, align);
}
+ ///
+ /// Registers the vitals "detail" icon overlays absorbed from the meter's
+ /// two slice containers (the 0x100004A9 children — health heart
+ /// 0x06007490/91, stamina sword 0x06007492/93, mana scepter 0x06007494/95).
+ /// Retail authors each icon TWICE: a dim version in the BACK container
+ /// (drawn unclipped over the empty track) and a bright version in the
+ /// FRONT container (fill-clipped with the rest of the front layer —
+ /// UIElement_Meter::DrawChildren @0x0046FBD0 clips the whole
+ /// m_pcChildImage child, element id 2, to the 0x69 fraction), so
+ /// the icon itself fills up with the vital. Both overlays author media
+ /// ONLY for ShowDetail (HideDetail authors File=0), so they
+ /// draw solely in that state. Rects are the overlays' authored X/Y/W/H
+ /// local to the meter (the containers span the meter at 0,0).
+ ///
+ internal void ConfigureDetailOverlay(
+ uint backSprite, float backX, float backY, float backW, float backH,
+ uint frontSprite, float frontX, float frontY, float frontW, float frontH,
+ bool passToChildren)
+ {
+ _detailBackSprite = backSprite;
+ _detailBackRect = (backX, backY, backW, backH);
+ _detailFrontSprite = frontSprite;
+ _detailFrontRect = (frontX, frontY, frontW, frontH);
+ _detailConfigured = backSprite != 0 || frontSprite != 0;
+ _detailPassToChildren = passToChildren;
+ }
+
public bool TrySetRetailState(uint stateId)
{
+ // Vitals detail toggle (gmVitalsUI::ListenToElementMessage @0x004BFC00
+ // flips the window root between HideDetail 0x10000006 and ShowDetail
+ // 0x10000007; the meter receives the state through the authored
+ // PassToChildren cascade — UIElement::SetState @0x00464E70). The
+ // meter's own HideDetail/ShowDetail StateDescs are media-less and
+ // exist purely to keep propagating (PassToChildren=true), so forward
+ // to the absorbed text child (whose per-state 0x3B hides the numbers
+ // in ShowDetail) and let OnDraw key the icon overlays off the state.
+ if (_detailConfigured
+ && stateId is RetailUiStateIds.HideDetail or RetailUiStateIds.ShowDetail)
+ {
+ ActiveRetailStateId = stateId;
+ if (_detailPassToChildren)
+ foreach (UiElement child in Children)
+ if (child is IUiDatStateful stateful)
+ stateful.TrySetRetailState(stateId);
+ return true;
+ }
+
bool hasFill = _stateFillSprites.TryGetValue(stateId, out uint spriteId);
bool hasLabel = _stateLabels.TryGetValue(stateId, out var caption);
if (!hasFill && !hasLabel)
@@ -198,9 +261,23 @@ public sealed class UiMeter : UiElement, IUiDatStateful
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
// The front carries its own right-cap (shown at 100%); clipping below 100%
// removes it and reveals the back track's right-cap — retail's scissor-fill.
+ //
+ // ShowDetail icon overlays ride their authored containers: the dim BACK
+ // icon draws over the full track (its 0x100004A9 child draws AFTER the
+ // three slice children — higher ReadOrder within the back container);
+ // the bright FRONT icon is clipped with the rest of the front layer to
+ // the fill fraction (retail clips the whole element-id-2 child).
+ bool detail = _detailConfigured
+ && ActiveRetailStateId == RetailUiStateIds.ShowDetail;
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
+ if (detail)
+ DrawDetailIcon(ctx, resolve, _detailBackSprite, _detailBackRect, Width);
if (pct is not null && p > 0f)
+ {
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
+ if (detail)
+ DrawDetailIcon(ctx, resolve, _detailFrontSprite, _detailFrontRect, Width * p);
+ }
}
}
else
@@ -302,6 +379,30 @@ public sealed class UiMeter : UiElement, IUiDatStateful
ctx.DrawSprite(tex, 0f, y, w, visibleH, 0f, v0, 1f, v1, System.Numerics.Vector4.One);
}
+ ///
+ /// Draws one ShowDetail icon overlay at its authored meter-local rect,
+ /// horizontally clipped to local px (the back icon
+ /// passes the full width; the front icon passes Width * fraction,
+ /// mirroring retail's whole-front-container fill clip). The visible portion
+ /// is UV-cropped so the icon reveals left-to-right with the fill. Height is
+ /// clamped to the meter's box — retail clips children to the parent rect
+ /// (the stamina FRONT overlay authors H=28 in a 16px bar; retail shows 16).
+ ///
+ private void DrawDetailIcon(
+ UiRenderContext ctx, Func resolve,
+ uint spriteId, (float X, float Y, float W, float H) rect, float clipW)
+ {
+ if (spriteId == 0 || rect.W <= 0f || rect.H <= 0f) return;
+ var (tex, _, _) = resolve(spriteId);
+ if (tex == 0) return;
+ float visibleW = MathF.Min(rect.W, clipW - rect.X);
+ if (visibleW <= 0f) return;
+ float h = MathF.Min(rect.H, Height - rect.Y);
+ if (h <= 0f) return;
+ float u1 = visibleW / rect.W;
+ ctx.DrawSprite(tex, rect.X, rect.Y, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
+ }
+
/// Draw a slice over local [,
/// pieceX+], with the texture repeating every
/// px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs
index 5bd2ab0e..1a9f64e3 100644
--- a/src/AcDream.App/UI/UiText.cs
+++ b/src/AcDream.App/UI/UiText.cs
@@ -386,6 +386,22 @@ public sealed class UiText : UiElement, IUiDatStateful
&& TryColor(color, out Vector4 resolvedColor))
DefaultColor = resolvedColor;
+ // Per-state Invisible (dat property 0x3B): retail's SetState applies the
+ // committed state's properties through UIElement::OnSetAttribute, whose
+ // case 8 (@0x00462DAE, property id 0x33 + 8 = 0x3B) is
+ // `SetVisible(value == 0)`. Same NAMED-states-only scoping as
+ // UiDatElement.TrySetRetailState (a DirectState 0x3B is the
+ // construction-time "authored invisible" class — #408, separately
+ // gated). First consumer here: the vitals cur/max number labels
+ // (0x100000EB/ED/EF) author HideDetail={0x3B:false} /
+ // ShowDetail={0x3B:true} — the numbers hide when the click toggle
+ // switches the window to the graphical icon mode.
+ if (stateId != UiStateInfo.DirectStateId
+ && state is not null
+ && state.Properties.Values.TryGetValue(0x3Bu, out var invisibleProp)
+ && invisibleProp.Kind == UiPropertyKind.Bool)
+ Visible = !invisibleProp.BoolValue;
+
// Retail's state cascade also swaps the element's AUTHORED string
// when the incoming state carries its own 0x17 (the friends row's
// status cell: 'Online'/'Offline' with per-state colors). Resolved
diff --git a/tests/AcDream.App.Tests/UI/Layout/VitalsDetailToggleTests.cs b/tests/AcDream.App.Tests/UI/Layout/VitalsDetailToggleTests.cs
new file mode 100644
index 00000000..75fb4e4f
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/VitalsDetailToggleTests.cs
@@ -0,0 +1,199 @@
+using AcDream.App.UI;
+using AcDream.App.UI.Layout;
+
+namespace AcDream.App.Tests.UI.Layout;
+
+///
+/// The retail vitals numeric/graphical click toggle, against the committed
+/// vitals fixture (vitals_2100006C.json) — no dats, no GL.
+///
+/// Retail mechanism (fully derived 2026-08-17):
+///
+/// - gmVitalsUI::ListenToElementMessage @0x004BFC00 —
+/// mouse press (msg 0x1C) with dwParam1 7 (left) or 0xA (right) flips
+/// SetState(m_state == HideDetail ? ShowDetail : HideDetail).
+/// - UIElement::SetState @0x00464E70 — the state
+/// cascades through the authored PassToChildren chain (root and meters
+/// author media-less HideDetail/ShowDetail with PassToChildren=true).
+/// - The cur/max labels author per-state 0x3B (invisible —
+/// UIElement::OnSetAttribute case 8 @0x00462DAE is
+/// SetVisible(value == 0)): HideDetail={0x3B:false} (numbers shown),
+/// ShowDetail={0x3B:true} (numbers hidden).
+/// - The 0x100004A9 overlay children author icon media only
+/// for ShowDetail (heart 0x06007490/91, sword 0x06007492/93, scepter
+/// 0x06007494/95) — dim back icon unclipped, bright front icon fill-clipped.
+/// - UIElement_Dragbar @0x0046C850 /
+/// UIElement_Resizebar @0x0046B930 consume presses (return 2) — a
+/// window move/resize press never toggles.
+/// - Not persisted: gmGamePlayUI::SaveScreenLayout
+/// @0x004EAD50 writes window rects only; each window resets to the
+/// authored Undef default per session.
+///
+///
+[Trait("Category", "Conformance")]
+public class VitalsDetailToggleTests
+{
+ private const uint DragBarTop = 0x1000063Cu; // Type 2 — WindowMoveHandle
+ private const uint GripTopLeft = 0x1000063Bu; // Type 9 — UiResizeGrip
+
+ // ── Import shape ─────────────────────────────────────────────────────────
+
+ [Fact]
+ public void VitalsTree_RootIsVitalsRootWidget()
+ {
+ var layout = FixtureLoader.LoadVitals();
+ Assert.IsType(layout.Root);
+ }
+
+ [Fact]
+ public void VitalsTree_MetersAbsorbDetailIconOverlays()
+ {
+ var layout = FixtureLoader.LoadVitals();
+
+ // MeterId → (dim back icon, bright front icon) from the authored
+ // 0x100004A9 ShowDetail media (format doc §11 + installed-DAT probe).
+ (uint MeterId, uint Back, uint Front)[] cases =
+ [
+ (VitalsController.Health, 0x06007490u, 0x06007491u), // heart
+ (VitalsController.Stamina, 0x06007492u, 0x06007493u), // sword
+ (VitalsController.Mana, 0x06007494u, 0x06007495u), // scepter
+ ];
+
+ foreach (var (meterId, back, front) in cases)
+ {
+ var m = Assert.IsType(layout.FindElement(meterId));
+ Assert.True(m.HasDetailOverlay);
+ Assert.Equal(back, m.DetailBackSprite);
+ Assert.Equal(front, m.DetailFrontSprite);
+ }
+
+ // Health's authored overlay rect: 18x16 at x=66 (the heart).
+ var health = Assert.IsType(layout.FindElement(VitalsController.Health));
+ Assert.Equal((66f, 0f, 18f, 16f), health.DetailBackRect);
+ }
+
+ // ── The press toggle ─────────────────────────────────────────────────────
+
+ [Fact]
+ public void Press_TogglesUndefThenHideDetailThenShowDetail()
+ {
+ var layout = FixtureLoader.LoadVitals();
+ var root = Assert.IsType(layout.Root);
+ var stamina = Assert.IsType(layout.FindElement(VitalsController.Stamina));
+ var staminaText = Assert.IsType(layout.FindElement(VitalsController.StaminaText));
+
+ // Login default: authored DefaultState is Undef and nothing calls
+ // SetState — numbers visible, no icons.
+ Assert.True(staminaText.Visible);
+
+ // Press 1 (from Undef): retail lands on HideDetail — visually identical
+ // (labels author {0x3B:false} there), never back to Undef afterwards.
+ Press(root, root);
+ Assert.Equal(RetailUiStateIds.HideDetail, root.ActiveRetailStateId);
+ Assert.Equal(RetailUiStateIds.HideDetail, stamina.ActiveRetailStateId);
+ Assert.True(staminaText.Visible);
+
+ // Press 2: ShowDetail — the graphical mode. Numbers hidden, icons on.
+ Press(root, root);
+ Assert.Equal(RetailUiStateIds.ShowDetail, root.ActiveRetailStateId);
+ Assert.Equal(RetailUiStateIds.ShowDetail, stamina.ActiveRetailStateId);
+ Assert.False(staminaText.Visible);
+
+ // Press 3: back to numeric.
+ Press(root, root);
+ Assert.Equal(RetailUiStateIds.HideDetail, root.ActiveRetailStateId);
+ Assert.True(staminaText.Visible);
+ }
+
+ [Fact]
+ public void RightPress_TogglesLikeLeftPress()
+ {
+ // Retail accepts dwParam1 7 (left) OR 0xA (right) — @0x004BFC19.
+ var layout = FixtureLoader.LoadVitals();
+ var root = Assert.IsType(layout.Root);
+
+ Press(root, root, UiEventType.RightDown);
+ Assert.Equal(RetailUiStateIds.HideDetail, root.ActiveRetailStateId);
+ Press(root, root, UiEventType.RightDown);
+ Assert.Equal(RetailUiStateIds.ShowDetail, root.ActiveRetailStateId);
+ }
+
+ [Fact]
+ public void Press_CascadesToAllThreeLabels()
+ {
+ var layout = FixtureLoader.LoadVitals();
+ var root = Assert.IsType(layout.Root);
+
+ Press(root, root); // Undef → HideDetail
+ Press(root, root); // HideDetail → ShowDetail
+ foreach (uint textId in new[]
+ {
+ VitalsController.HealthText,
+ VitalsController.StaminaText,
+ VitalsController.ManaText,
+ })
+ {
+ var text = Assert.IsType(layout.FindElement(textId));
+ Assert.False(text.Visible);
+ }
+ }
+
+ // ── Chrome exclusions ────────────────────────────────────────────────────
+
+ [Fact]
+ public void Press_OnDragBar_DoesNotToggle()
+ {
+ // Retail UIElement_Dragbar::ListenToElementMessage returns 2 (consumed)
+ // for every message — the press never reaches gmVitalsUI.
+ var layout = FixtureLoader.LoadVitals();
+ var root = Assert.IsType(layout.Root);
+ var dragBar = layout.FindElement(DragBarTop);
+ Assert.NotNull(dragBar);
+ Assert.True(dragBar!.WindowMoveHandle);
+
+ uint before = root.ActiveRetailStateId;
+ Press(root, dragBar);
+ Assert.Equal(before, root.ActiveRetailStateId);
+ }
+
+ [Fact]
+ public void Press_OnResizeGrip_DoesNotToggle()
+ {
+ // Retail UIElement_Resizebar::ListenToElementMessage returns 2 likewise.
+ var layout = FixtureLoader.LoadVitals();
+ var root = Assert.IsType(layout.Root);
+ var grip = layout.FindElement(GripTopLeft);
+ Assert.NotNull(grip);
+ Assert.IsType(grip);
+
+ uint before = root.ActiveRetailStateId;
+ Press(root, grip!);
+ Assert.Equal(before, root.ActiveRetailStateId);
+ }
+
+ // ── Independence: each window keeps its own state ─────────────────────────
+
+ [Fact]
+ public void TwoWindows_ToggleIndependently()
+ {
+ // Retail: gmFloatyVitalsUI and gmFloatySideVitalsUI each carry their
+ // own m_state; toggling one never touches the other.
+ var a = FixtureLoader.LoadVitals();
+ var b = FixtureLoader.LoadVitals();
+ var rootA = Assert.IsType(a.Root);
+ var rootB = Assert.IsType(b.Root);
+
+ Press(rootA, rootA);
+ Press(rootA, rootA);
+ Assert.Equal(RetailUiStateIds.ShowDetail, rootA.ActiveRetailStateId);
+ Assert.NotEqual(RetailUiStateIds.ShowDetail, rootB.ActiveRetailStateId);
+ }
+
+ // ── Helpers ──────────────────────────────────────────────────────────────
+
+ /// Delivers a press to as UiRoot's bubble
+ /// walk would: the hit target rides in .
+ private static void Press(UiVitalsRoot root, UiElement target,
+ int type = UiEventType.MouseDown)
+ => root.OnEvent(new UiEvent(target.EventId, target, type));
+}