From 5f924937427f84d6c224e07ae53b94290d665c55 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 26 Jun 2026 17:08:24 +0200 Subject: [PATCH] fix(studio): switch character skills tab on visible list --- .../UI/Layout/CharacterStatController.cs | 29 +++++++- .../UI/Layout/CharacterStatControllerTests.cs | 66 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 09d14335..4fb6c0c4 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -399,7 +399,9 @@ public static class CharacterStatController // last duplicate) so we get the wider-label copies (195px) for the unselected state. BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel); - UiElement? statList = layout.FindElement(ListBoxId); + UiElement? statList = + FindInPageContaining(layout, NameId, static el => HasDatElementId(el, ListBoxId)) + ?? layout.FindElement(ListBoxId); RebuildActiveList(); if (layout.Root is { } tabRoot2 && spriteResolve is not null) @@ -1228,6 +1230,31 @@ public static class CharacterStatController return null; } + /// Finds the first descendant matching inside the + /// root-level tab page that contains . This is required for + /// duplicated tab-page ids such as the character list box (0x1000023D): + /// stores only the last duplicate in its id dictionary, which may be in a hidden page. + private static UiElement? FindInPageContaining( + ImportedLayout layout, + uint anchorId, + Func predicate) + { + if (layout.FindElement(anchorId) is not { } anchor || layout.Root is not { } root) + return null; + + foreach (var page in root.Children) + { + if (page.Children.Count == 0) continue; + if (!ContainsWidget(page, anchor)) continue; + return FindInSubtree(page, predicate); + } + + return null; + } + + private static bool HasDatElementId(UiElement element, uint id) + => element is UiDatElement datElement && datElement.ElementId == id; + private static bool ContainsWidget(UiElement node, UiElement target) { if (ReferenceEquals(node, target)) return true; diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index 403041a2..a9dac56d 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -678,6 +678,46 @@ public class CharacterStatControllerTests Assert.Equal("Strength", rows[0].Children.OfType().ToList()[1].LinesProvider()[0].Text); } + [Fact] + public void SkillsTab_MouseClick_RebuildsVisibleListWhenListIdIsDuplicated() + { + var root = new UiPanel { Width = 300, Height = 600 }; + var attrPage = new UiPanel { Top = 25, Width = 300, Height = 575 }; + var hiddenPage = new UiPanel { Top = 25, Width = 300, Height = 575 }; + var name = new UiText(); + var visibleList = MakeDatElement(CharacterStatController.ListBoxId, top: 112, width: 300, height: 398); + var hiddenDuplicateList = MakeDatElement(CharacterStatController.ListBoxId, top: 112, width: 300, height: 398); + + attrPage.AddChild(name); + attrPage.AddChild(visibleList); + hiddenPage.AddChild(hiddenDuplicateList); + root.AddChild(attrPage); + root.AddChild(hiddenPage); + + var layout = new ImportedLayout(root, new Dictionary + { + [CharacterStatController.NameId] = name, + // Mirrors the real import: the id dictionary can point at a hidden duplicate. + [CharacterStatController.ListBoxId] = hiddenDuplicateList, + }); + + CharacterStatController.Bind(layout, SampleData.SampleCharacter, + spriteResolve: id => (id, 16, 16)); + + Assert.Equal("Strength", FirstRowName(visibleList)); + Assert.Equal("", FirstRowName(hiddenDuplicateList)); + + var ui = new UiRoot { Width = 300, Height = 600 }; + ui.AddChild(root); + Assert.IsType(ui.Pick(132, 12)); + + ui.OnMouseDown(UiMouseButton.Left, 132, 12); + ui.OnMouseUp(UiMouseButton.Left, 132, 12); + + Assert.Equal("Melee Defense", FirstRowName(visibleList)); + Assert.Equal("", FirstRowName(hiddenDuplicateList)); + } + [Fact] public void SkillsTab_SelectWarMagic_ShowsTrainedSkillFooter() { @@ -1027,6 +1067,32 @@ public class CharacterStatControllerTests tab.OnClick!(); } + private static string FirstRowName(UiElement list) + { + var row = list.Children.OfType().FirstOrDefault(); + if (row is null) return ""; + var texts = row.Children.OfType().ToList(); + return texts.Count > 1 ? texts[1].LinesProvider()[0].Text : ""; + } + + private static UiDatElement MakeDatElement(uint id, float top, float width, float height) + { + var info = new ElementInfo + { + Id = id, + Type = 3, + Y = top, + Width = width, + Height = height, + }; + return new UiDatElement(info, static _ => (0u, 0, 0)) + { + Top = top, + Width = width, + Height = height, + }; + } + /// Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites). private static UiButton MakeButton() {