From 4bb37e302edf3f749f440d941e4178e7cb7cb86d Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Jul 2026 18:29:55 +0200 Subject: [PATCH] fix(ui): bind visible skills scrollbar --- .../UI/Layout/CharacterStatController.cs | 22 ++++++++++++++++--- .../UI/Layout/CharacterStatControllerTests.cs | 9 ++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 9dfc7211..c588e2bf 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -621,8 +621,13 @@ public static class CharacterStatController if (spriteResolve is null) return null; - UiElement? source = - (contentPage is not null + // The mounted character sublayout contains duplicated stat-management + // branches with the same element ids. Bind the scrollbar adjacent to the + // active list first; a page-wide DFS can select an inactive duplicate that + // is never drawn while leaving the visible gutter unconfigured. + UiElement? source = statList?.Parent?.Children.FirstOrDefault( + static el => HasDatElementId(el, ListScrollbarId)) + ?? (contentPage is not null ? FindInSubtree(contentPage, static el => HasDatElementId(el, ListScrollbarId)) : null) ?? layout.FindElement(ListScrollbarId); @@ -1674,7 +1679,17 @@ public static class CharacterStatController => DatElementId(element) == id; private static uint DatElementId(UiElement element) - => element switch + { + // DatWidgetFactory assigns the canonical id on every imported widget, + // including behavioral types such as UiScrollbar that do not expose a + // type-specific ElementId property. Prefer it so subtree role lookup is + // independent of the concrete widget class. The fallbacks keep the small + // hand-built controller fixtures working when they construct widgets + // directly instead of through the factory. + if (element.DatElementId != 0u) + return element.DatElementId; + + return element switch { UiDatElement datElement => datElement.ElementId, UiButton button => button.ElementId, @@ -1682,6 +1697,7 @@ public static class CharacterStatController UiText text => text.ElementId, _ => 0u, }; + } private static UiElement? FindElementByDatId(ImportedLayout layout, UiElement? scope, uint id) { diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index 00a9c6ee..45233e60 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1501,9 +1501,18 @@ public class CharacterStatControllerTests Assert.Equal((510f, 7f), (divider.Top, divider.Height)); Assert.Equal(3, footers.Count); Assert.All(footers, footer => Assert.Equal((520f, 55f), (footer.Top, footer.Height))); + Assert.False( + scrollbar.Visible, + $"pre-skills scrollbar: model={scrollbar.Model is not null}, " + + $"resolve={scrollbar.SpriteResolve is not null}, track=0x{scrollbar.TrackSprite:X8}"); ClickTab(layout, left: 92f); Assert.True(scrollbar.Visible); + Assert.NotNull(scrollbar.Model); + Assert.NotNull(scrollbar.SpriteResolve); + Assert.Equal(0x06004C5Fu, scrollbar.TrackSprite); + Assert.Equal(0x06004C6Cu, scrollbar.UpSprite); + Assert.Equal(0x06004C69u, scrollbar.DownSprite); } // ── Helpers ──────────────────────────────────────────────────────────────