diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 867a5866..26876e41 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -410,7 +410,7 @@ public static class CharacterStatController return v.ToString(); }); - row.OnClick = () => HandleRowClick(rowIndex, sel, rows, data, allRaise1, allRaise10); + row.OnClick = () => HandleRowClick(rowIndex, sel, rows, spriteResolve, data, allRaise1, allRaise10); rows.Add(row); y += RowHeight; } @@ -437,7 +437,7 @@ public static class CharacterStatController }; }); - row.OnClick = () => HandleRowClick(absIndex, sel, rows, data, allRaise1, allRaise10); + row.OnClick = () => HandleRowClick(absIndex, sel, rows, spriteResolve, data, allRaise1, allRaise10); rows.Add(row); y += RowHeight; } @@ -453,6 +453,7 @@ public static class CharacterStatController int clickedIndex, int[] sel, List rows, + Func? spriteResolve, Func data, List allRaise1, List allRaise10) @@ -465,8 +466,35 @@ public static class CharacterStatController Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})"); // Update highlight on all rows. + // Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars) + // for the selected row background. When spriteResolve is available, apply the + // sprite; otherwise fall back to the translucent gold tint. + const uint HighlightSprite = 0x06001397u; for (int i = 0; i < rows.Count; i++) - rows[i].BackgroundColor = (i == newSel) ? HighlightBg : Vector4.Zero; + { + var row = rows[i]; + if (i == newSel) + { + if (spriteResolve is not null) + { + row.BackgroundColor = Vector4.Zero; + row.BackgroundSprite = HighlightSprite; + row.SpriteResolve = spriteResolve; + } + else + { + row.BackgroundColor = HighlightBg; + row.BackgroundSprite = 0u; + row.SpriteResolve = null; + } + } + else + { + row.BackgroundColor = Vector4.Zero; + row.BackgroundSprite = 0u; + row.SpriteResolve = null; + } + } // Update raise buttons. RefreshRaiseButtons(newSel, data, allRaise1, allRaise10); diff --git a/src/AcDream.App/UI/UiPanel.cs b/src/AcDream.App/UI/UiPanel.cs index a6c3269d..a927ec57 100644 --- a/src/AcDream.App/UI/UiPanel.cs +++ b/src/AcDream.App/UI/UiPanel.cs @@ -24,10 +24,28 @@ public class UiPanel : UiElement public float BorderThickness { get; set; } = 1f; + /// Optional dat RenderSurface id for the panel background sprite, drawn + /// in place of (or alongside) . 0 = none. + /// When set, the sprite is stretched to fill the panel rect. + /// Used by the attribute-list selected-row highlight (sprite 0x06001397 = Button state 6). + public uint BackgroundSprite { get; set; } + + /// Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height). + /// Required when is non-zero. + public Func? SpriteResolve { get; set; } + protected override void OnDraw(UiRenderContext ctx) { - if (BackgroundColor.W > 0f) + if (BackgroundSprite != 0 && SpriteResolve is { } sr) + { + var (tex, tw, th) = sr(BackgroundSprite); + if (tex != 0 && tw != 0 && th != 0) + ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); + } + else if (BackgroundColor.W > 0f) + { ctx.DrawRect(0, 0, Width, Height, BackgroundColor); + } if (BorderColor.W > 0f && BorderThickness > 0f) ctx.DrawRectOutline(0, 0, Width, Height, BorderColor, BorderThickness); diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index a875878c..d0607b97 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -433,6 +433,45 @@ public class CharacterStatControllerTests Assert.Equal(0f, rows[2].BackgroundColor.W); } + [Fact] + public void RowClick_WithSpriteResolve_SelectedRowHasHighlightSprite() + { + // When spriteResolve is provided, the selected row must use sprite 0x06001397 + // (retail Button-state-6 dark bar) instead of the translucent gold BackgroundColor. + var list = new UiPanel(); + var layout = Fake((CharacterStatController.ListBoxId, list)); + + // Minimal sprite resolver — returns a fake non-zero handle so UiPanel draws it. + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); + + CharacterStatController.Bind(layout, SampleData.SampleCharacter, + spriteResolve: FakeResolve); + + var rows = list.Children.OfType().ToList(); + rows[2].OnClick!(); + + Assert.Equal(0x06001397u, rows[2].BackgroundSprite); // selected → sprite + Assert.Equal(0f, rows[2].BackgroundColor.W); // no tint + Assert.Equal(0u, rows[0].BackgroundSprite); // others cleared + Assert.Equal(0u, rows[1].BackgroundSprite); + } + + [Fact] + public void RowClick_WithSpriteResolve_Deselect_ClearsSprite() + { + var list = new UiPanel(); + var layout = Fake((CharacterStatController.ListBoxId, list)); + static (uint, int, int) FakeResolve(uint id) => (1u, 32, 8); + CharacterStatController.Bind(layout, SampleData.SampleCharacter, + spriteResolve: FakeResolve); + + var rows = list.Children.OfType().ToList(); + rows[2].OnClick!(); // select + rows[2].OnClick!(); // deselect + + Assert.Equal(0u, rows[2].BackgroundSprite); + } + // ── Pass 2: Raise button affordability ─────────────────────────────────── [Fact]