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() {