diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index b726df79..86b037e4 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -315,6 +315,31 @@ public static class DatWidgetFactory
if (slices.Length > 0) bar.ThumbTopSprite = DefaultImage(slices[0]);
if (slices.Length > 1) bar.ThumbSprite = DefaultImage(slices[1]);
if (slices.Length > 2) bar.ThumbBotSprite = DefaultImage(slices[^1]);
+
+ // R3-4/R3-7 (Campaign CC gate round 1 re-test 2): retail authors
+ // TWO distinct thumb shapes for UIElement_Scrollbar (Type 11) —
+ // chat's own scrollbar (0x10000012) is the 3-slice composite the
+ // block above was built against (the thumb CHILD carries no media
+ // of its own; three Type-3 grandchildren supply the top-cap/
+ // middle/bottom-cap sprites) — but the chargen Skills listbox
+ // (0x100003f8), Summary's OVERVIEW listbox (0x10000401), and the
+ // Summary how-to box (0x100002e7 under 0x10000404) all author a
+ // SIMPLE single-sprite thumb instead: the SAME structural child
+ // (Type 1, id 1, not the inc/dec button) carries its OWN direct
+ // Normal/Normal_rollover/Normal_pressed media and has ZERO
+ // children (live-DAT-probe-confirmed against all three — no
+ // slice grandchildren to find, so `slices` above is always
+ // empty for this shape and every Thumb*Sprite stayed 0,
+ // matching the reported "track+arrows render, no thumb"
+ // symptom). already falls back
+ // to a single tiled `ThumbSprite` blit when the cap sprites are
+ // unset (`ThumbTopSprite != 0 && ThumbBotSprite != 0` gate), so
+ // the only missing piece is feeding it the thumb's OWN media
+ // when it has no slice children — additive: a thumb WITH real
+ // slice children (chat) is unaffected since `slices.Length == 0`
+ // is false for that shape.
+ if (slices.Length == 0)
+ bar.ThumbSprite = DefaultImage(thumb);
}
return bar;
diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
index e9066456..f6d78b60 100644
--- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
@@ -637,6 +637,109 @@ public class DatWidgetFactoryTests
Assert.Equal(0u, bar.ThumbSprite);
}
+ ///
+ /// R3-4/R3-7 (Campaign CC gate round 1 re-test 2): a VERTICAL scrollbar
+ /// whose thumb child carries its OWN direct media (Normal/
+ /// Normal_rollover/Normal_pressed) and has NO slice children — the
+ /// exact live-DAT shape of the chargen Skills listbox scrollbar
+ /// (0x100003f8), Summary's OVERVIEW listbox scrollbar (0x10000401),
+ /// and the how-to box's own scrollbar (0x100002e7). Before this fix
+ /// ThumbSprite stayed 0 (the 3-slice-children search this factory was
+ /// originally built against — chat's scrollbar shape, see
+ ///
+ /// — found nothing to select), so the track and arrows rendered but
+ /// the thumb never did, regardless of overflow.
+ ///
+ [Fact]
+ public void Type11_VerticalScrollbar_SingleSpriteThumbWithNoSliceChildren_SetsThumbSprite()
+ {
+ const uint Thumb = 0x06005A11u;
+ const uint DecrementId = 0x10000071u;
+ const uint IncrementId = 0x10000072u;
+ var decrement = new ElementInfo { Id = DecrementId, Type = 1u, Y = 83f, Width = 37f, Height = 17f };
+ decrement.StateMedia["Normal"] = (0x06004C69u, 1);
+ var increment = new ElementInfo { Id = IncrementId, Type = 1u, Y = 0f, Width = 37f, Height = 17f };
+ increment.StateMedia["Normal"] = (0x06004C6Cu, 1);
+ var thumb = new ElementInfo { Id = 1u, Type = 1u, Width = 37f, Height = 39f };
+ thumb.StateMedia["Normal"] = (Thumb, 1);
+ thumb.StateMedia["Normal_rollover"] = (0x06005A12u, 1);
+ thumb.StateMedia["Normal_pressed"] = (0x06005A13u, 1);
+ thumb.States[1u] = new UiStateInfo { Id = 1u, Name = "Normal", Image = new UiImageMedia(Thumb, 1) };
+
+ var info = new ElementInfo
+ {
+ Type = 11u,
+ Width = 37f,
+ Height = 307f,
+ Children = [decrement, increment, thumb],
+ };
+ var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
+ state.Properties.Values[0x77u] = new UiPropertyValue
+ { Kind = UiPropertyKind.Enum, UnsignedValue = IncrementId };
+ state.Properties.Values[0x78u] = new UiPropertyValue
+ { Kind = UiPropertyKind.Enum, UnsignedValue = DecrementId };
+ info.States[UiStateInfo.DirectStateId] = state;
+
+ var bar = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null));
+
+ Assert.False(bar.Horizontal);
+ Assert.Equal(Thumb, bar.ThumbSprite);
+ Assert.Equal(0u, bar.ThumbTopSprite);
+ Assert.Equal(0u, bar.ThumbBotSprite);
+ }
+
+ ///
+ /// Negative companion: a VERTICAL thumb WITH real 3-slice children
+ /// (chat's own shape) is unaffected by the R3-4/R3-7 fallback — the
+ /// `slices.Length == 0` gate never triggers, so ThumbSprite/Top/Bot
+ /// come from the slice children exactly as before.
+ ///
+ [Fact]
+ public void Type11_VerticalScrollbar_ThumbWithSliceChildren_StillUsesSliceMedia()
+ {
+ const uint Top = 0x06004C60u;
+ const uint Mid = 0x06004C63u;
+ const uint Bot = 0x06004C66u;
+ var decrement = new ElementInfo { Id = 0x10000071u, Type = 1u, Y = 0f, Width = 16f, Height = 16f };
+ decrement.StateMedia["Normal"] = (0x06004C69u, 1);
+ var increment = new ElementInfo { Id = 0x10000072u, Type = 1u, Y = 32f, Width = 16f, Height = 16f };
+ increment.StateMedia["Normal"] = (0x06004C6Cu, 1);
+ var thumb = new ElementInfo { Id = 1u, Type = 1u, Width = 16f, Height = 16f };
+ var topCap = new ElementInfo { Id = 0x10000364u, Type = 3u, Y = 0f, Width = 16f, Height = 3f };
+ topCap.StateMedia["Normal"] = (Top, 1);
+ topCap.States[1u] = new UiStateInfo { Id = 1u, Name = "Normal", Image = new UiImageMedia(Top, 1) };
+ var mid = new ElementInfo { Id = 0x10000365u, Type = 3u, Y = 3f, Width = 16f, Height = 10f };
+ mid.StateMedia["Normal"] = (Mid, 1);
+ mid.States[1u] = new UiStateInfo { Id = 1u, Name = "Normal", Image = new UiImageMedia(Mid, 1) };
+ var botCap = new ElementInfo { Id = 0x10000366u, Type = 3u, Y = 13f, Width = 16f, Height = 3f };
+ botCap.StateMedia["Normal"] = (Bot, 1);
+ botCap.States[1u] = new UiStateInfo { Id = 1u, Name = "Normal", Image = new UiImageMedia(Bot, 1) };
+ thumb.Children.Add(topCap);
+ thumb.Children.Add(mid);
+ thumb.Children.Add(botCap);
+
+ var info = new ElementInfo
+ {
+ Type = 11u,
+ Width = 16f,
+ Height = 73f,
+ Children = [decrement, increment, thumb],
+ };
+ var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
+ state.Properties.Values[0x77u] = new UiPropertyValue
+ { Kind = UiPropertyKind.Enum, UnsignedValue = 0x10000072u };
+ state.Properties.Values[0x78u] = new UiPropertyValue
+ { Kind = UiPropertyKind.Enum, UnsignedValue = 0x10000071u };
+ info.States[UiStateInfo.DirectStateId] = state;
+
+ var bar = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null));
+
+ Assert.False(bar.Horizontal);
+ Assert.Equal(Top, bar.ThumbTopSprite);
+ Assert.Equal(Mid, bar.ThumbSprite);
+ Assert.Equal(Bot, bar.ThumbBotSprite);
+ }
+
[Fact]
public void RetailToolbarFixture_buildsEditableStackEntry_andAuthoredHorizontalSlider()
{