diff --git a/src/AcDream.App/UI/RetailScrollbarChrome.cs b/src/AcDream.App/UI/RetailScrollbarChrome.cs
index 0dc3623d0..5c427027a 100644
--- a/src/AcDream.App/UI/RetailScrollbarChrome.cs
+++ b/src/AcDream.App/UI/RetailScrollbarChrome.cs
@@ -84,6 +84,27 @@ internal static class RetailScrollbarChrome
bar.ThumbBotPressedSprite = ThumbBotPressed;
}
+ ///
+ /// Wires the retail vertical skin onto a popup's own
+ /// procedural scrollbar properties (
+ /// etc). Owner live-client report 2026-09-07 ("For scrollable dropdown or
+ /// the meta window we use the same assets as we do in for example chat or
+ /// inventory window"): the popup's own DrawPopupScrollbar draws a
+ /// simpler Normal-only chrome (no hover/pressed states — matching how
+ /// VendorUiController/ConfigOptionsPageController already
+ /// wire these exact ids), so only the Normal-state constants are needed
+ /// here.
+ ///
+ internal static void ApplyToMenuPopup(UiMenu menu)
+ {
+ menu.ScrollTrackSprite = Track;
+ menu.ScrollThumbTopSprite = ThumbTopNormal;
+ menu.ScrollThumbSprite = ThumbMidNormal;
+ menu.ScrollThumbBottomSprite = ThumbBotNormal;
+ menu.ScrollUpSprite = UpNormal;
+ menu.ScrollDownSprite = DownNormal;
+ }
+
/// Wires the full retail horizontal skin onto .
/// The leading () slot is the LEFT edge.
internal static void ApplyHorizontal(UiScrollbar bar)
diff --git a/src/AcDream.App/UI/UiMenu.cs b/src/AcDream.App/UI/UiMenu.cs
index 95a03f664..f183310b4 100644
--- a/src/AcDream.App/UI/UiMenu.cs
+++ b/src/AcDream.App/UI/UiMenu.cs
@@ -884,9 +884,19 @@ public sealed class UiMenu : UiElement
}
/// Plain counterpart of — same
- /// -sliced single column, plain
- /// selected/hover row fills, and a plain scrollbar
- /// () instead of the sprite chrome.
+ /// -sliced single column and plain
+ /// selected/hover row fills, but the SCROLLBAR itself draws retail's own
+ /// chrome (owner live-client report 2026-09-07: "For scrollable dropdown
+ /// or the meta window we use the same assets as we do in for example
+ /// chat or inventory window") via the shared
+ /// helper — the exact ids
+ /// wires onto etc, the SAME sprite ids the
+ /// chat SpewBox/inventory scrollbar uses. Rows
+ /// stay plain by design (the owner accepted the flat dark list; only the
+ /// bar was objectionable). A menu built with no
+ /// at all (a hand-built test/legacy fixture) falls back to the fully
+ /// flat rather than silently
+ /// drawing nothing.
private void DrawScrollablePopupPlain(UiRenderContext ctx)
{
ConfigurePopupScroll();
@@ -918,17 +928,26 @@ public sealed class UiMenu : UiElement
avail ? PlainTextColor : TextColorGhosted);
}
- DrawPopupScrollbarPlain(ctx, inX + ColumnWidth, inY);
+ if (SpriteResolve is { } resolve)
+ DrawPopupScrollbar(ctx, resolve, inX + ColumnWidth, inY);
+ else
+ DrawPopupScrollbarPlain(ctx, inX + ColumnWidth, inY);
}
///
- /// Plain counterpart of : a 1px-bordered
- /// track and a flat thumb, both in — no DAT
- /// thumb/track/arrow-button art at all. Shares the exact same
- /// geometry (so the thumb's drawn
- /// position matches 's hit-test
- /// math), but draws no separate up/down button glyphs — plain mode has no
- /// art for them and the click regions already work through geometry alone
+ /// NO-RESOLVER FALLBACK ONLY (see 's
+ /// own doc comment — the 2026-09-07 owner directive moved the normal
+ /// plain-popup scrollbar to retail's own chrome via
+ /// ). This draws a 1px-bordered track and
+ /// a flat thumb, both in — no DAT
+ /// thumb/track/arrow-button art at all — for the rare case a
+ /// is built with true but no
+ /// at all (a hand-built test/legacy fixture).
+ /// Shares the exact same geometry (so
+ /// the thumb's drawn position matches
+ /// 's hit-test math), but
+ /// draws no separate up/down button glyphs — plain mode has no art for
+ /// them and the click regions already work through geometry alone
/// ( is unchanged).
///
private void DrawPopupScrollbarPlain(UiRenderContext ctx, float x, float y)
diff --git a/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs b/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
index 66c5bf689..98d097386 100644
--- a/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
+++ b/tests/AcDream.App.Tests/UI/UiMenuPlainStyleTests.cs
@@ -359,8 +359,17 @@ public sealed class UiMenuPlainStyleTests
Assert.Equal(RetailChromeSprites.Border + UiMenu.PlainPadding, glyphSeg.Verts[0], 3);
}
+ // ── Owner live-client report 2026-09-07 ("For scrollable dropdown or the
+ // meta window we use the same assets as we do in for example chat or
+ // inventory window"): the plain popup's SCROLLBAR now draws retail's own
+ // chrome (the exact sprite ids RetailScrollbarChrome wires onto
+ // chat/inventory's own bar) — only the ROWS stayed plain. These two tests
+ // used to pin a fully flat/untextured scrollbar; they now pin the
+ // opposite: real sprite draws for the bar, untouched plain fills for the
+ // rows, and no visible bar at all once the content fits.
+
[Fact]
- public void Plain_OpenPopup_ScrollableOverflow_DrawsPlainTrackAndFlatThumb_NoDatArt()
+ public void Plain_OpenPopup_ScrollableOverflow_DrawsRetailScrollbarChrome_RowsStayPlain()
{
int resolveCalls = 0;
var menu = MakePopupMenu(retailButtonArt: false, itemCount: 12, rowsPerColumn: 5, scrollable: true,
@@ -373,27 +382,39 @@ public sealed class UiMenuPlainStyleTests
var segs = renderer.DebugSpriteSegmentVerts;
Assert.True(menu.PopupScroll.HasOverflow);
- Assert.Equal(0, resolveCalls);
- Assert.Equal(0, QuadCount(segs, menu.ScrollTrackSprite));
- Assert.Equal(0, QuadCount(segs, menu.ScrollThumbSprite));
+ // Track + up + down + thumb top/mid/bottom = 6 resolved sprite ids —
+ // the SAME chrome ids the chat/inventory scrollbar resolves through
+ // the same SpriteResolve seam, no longer the flat DrawFill-only path.
+ Assert.Equal(6, resolveCalls);
+ Assert.Equal(1, QuadCount(segs, menu.ScrollTrackSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollUpSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollDownSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollThumbTopSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollThumbSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollThumbBottomSprite));
float outerTop = menu.Height;
float inX = RetailChromeSprites.Border, inY = outerTop + RetailChromeSprites.Border;
- float scrollbarX = inX + PlainColumnWidth;
+ // The rows are untouched by the chrome swap: still a plain fill, no
+ // DAT row/checkbox art at all (RetailButtonArt=false's own contract).
Assert.True(HasFillQuad(segs, inX, inY, PlainColumnWidth, PlainRowHeight, menu.PlainSelectedColor),
- "expected visible row 0 (selected/current) filled with PlainSelectedColor");
- Assert.True(HasFillQuad(segs, scrollbarX, inY, menu.ScrollbarWidth, 5 * PlainRowHeight, menu.PlainBackgroundColor),
- "expected the scrollbar track background fill");
+ "expected visible row 0 (selected/current) still filled with PlainSelectedColor");
+ Assert.Equal(0, QuadCount(segs, menu.ItemHighlightSprite));
+ Assert.Equal(0, QuadCount(segs, menu.ItemNormalSprite));
- // popup bg(1)+outline(4) + selected row(1) + scrollbar bg(1)+outline(4) + thumb(1) = 12.
- Assert.Equal(12, QuadCount(segs, 0u));
+ // popup bg(1)+outline(4) + selected row(1) = 6 untextured quads;
+ // the scrollbar itself no longer contributes any (it is all sprite
+ // draws now).
+ Assert.Equal(6, QuadCount(segs, 0u));
}
[Fact]
- public void Plain_ScrollablePopup_ContentFits_DrawsTrackWithNoThumb()
+ public void Plain_ScrollablePopup_ContentFits_DrawsNoScrollbarAtAll()
{
- var menu = MakePopupMenu(retailButtonArt: false, itemCount: 3, rowsPerColumn: 5, scrollable: true);
+ int resolveCalls = 0;
+ var menu = MakePopupMenu(retailButtonArt: false, itemCount: 3, rowsPerColumn: 5, scrollable: true,
+ countResolveCall: n => resolveCalls += n);
OpenAndHover(menu);
var (renderer, ctx) = MakeContext(200f, 200f);
@@ -402,9 +423,18 @@ public sealed class UiMenuPlainStyleTests
Assert.False(menu.PopupScroll.HasOverflow);
- // popup bg(1)+outline(4) + scrollbar bg(1)+outline(4) = 10, no thumb quad
- // (nothing selected/hovered here either).
- Assert.Equal(10, QuadCount(segs, 0u));
+ // Content-fits still draws the track + up/down buttons (retail's own
+ // proportion-0x88-defaults-to-1.0 rule — a content-fits bar shows a
+ // full-track thumb elsewhere in this class), but no thumb: 3 resolves.
+ Assert.Equal(3, resolveCalls);
+ Assert.Equal(1, QuadCount(segs, menu.ScrollTrackSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollUpSprite));
+ Assert.Equal(1, QuadCount(segs, menu.ScrollDownSprite));
+ Assert.Equal(0, QuadCount(segs, menu.ScrollThumbSprite));
+
+ // popup bg(1)+outline(4) = 5 untextured quads (nothing
+ // selected/hovered here either, and the scrollbar draws no fills).
+ Assert.Equal(5, QuadCount(segs, 0u));
}
[Fact]