fix #375: Configure Keyboard live mount — string resolver + parked template prototypes
Campaign OP gate 2: the screen opened as a visual mess (textless buttons/tabs, buttons above the window, overlapping text) while the fixture conformance suite stayed green — the #372 class again. Two root causes, both proven by the new env-gated live-DAT probe before fixing: 1. MountKeyboardConfig's main LayoutImporter.Build was the ONE mount in RetailUiRuntime not passing strings.Resolve — every AUTHORED caption (OK/Cancel/Defaults/Revert/Load/Save, the six ActionClass tab labels, the Command/Mapping column headers) built empty, while the controller's own resolveString row captions worked, which is why the screen was recognizable but textless. Fixed by passing the resolver like every sibling mount. 2. gmKeyboardUI authors its ListBox row templates (header 0x1000002E, action row 0x1000002F with the three 100x32 key buttons) as TOP-LEVEL siblings referenced by dat property 0x64. Retail never instantiates template-list elements as live widgets (AddItemFromTemplateList clones from the desc — the same re-import UiTemplateListBox's TemplateResolver performs), but ImportInfos built them parked at the screen's (0,0): three key buttons at y=0..32 ABOVE the framed panel (top y=62) — the 'outside the window' buttons — under a 570x40 header text overlapping them and the top chrome. ImportInfos now skips top-level elements referenced by a SAME-LAYOUT template list (the same skip class as the existing BaseElement-prototype filter; same-layout only because element ids collide across layouts — 0x10000211 is a page in BOTH the options and keyboard layouts). The probe (ACDREAM_PROBE_LIVE_MOUNT=1) pins both against the real DATs: prototypes absent from the built tree, and Defaults/Revert/OK/Cancel resolving on the resolver-passing build. Post-fix the import collapses to the framed 600x476 panel with every screen button inside its bounds. Full Release suite: 13,082 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
355c86a6f6
commit
8bd7e3b88d
5 changed files with 302 additions and 23 deletions
|
|
@ -44,29 +44,50 @@ modes; validate against the monitor's mode list first).
|
||||||
|
|
||||||
## #375 — Configure Keyboard screen renders as a visual mess at the live mount (missing button/tab captions, buttons outside the window, overlapping text)
|
## #375 — Configure Keyboard screen renders as a visual mess at the live mount (missing button/tab captions, buttons outside the window, overlapping text)
|
||||||
|
|
||||||
**Status:** OPEN — filed 2026-08-11 at Campaign OP's second connected gate
|
**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's
|
||||||
(user report, verbatim observations): "all buttons lacked descriptive
|
re-gate. Filed 2026-08-11 at Campaign OP's second connected gate (user
|
||||||
text", "some buttons were outside of the window", "lacked text in the
|
report, verbatim): "all buttons lacked descriptive text", "some buttons
|
||||||
tabs", "text next to the buttons was overlapping. Looked like a mess."
|
were outside of the window", "lacked text in the tabs", "text next to
|
||||||
The screen OPENED (the `[options] gameplay button 0x10000204 clicked`
|
the buttons was overlapping. Looked like a mess."
|
||||||
log line fired and the user saw the window), so the OP8 mount/wiring is
|
|
||||||
alive — the defect family is presentation at the LIVE mount.
|
|
||||||
|
|
||||||
**Same false-negative class as #372:** `KeyboardConfigControllerTests`
|
**TWO root causes, both proven by the live-DAT probe**
|
||||||
runs green against the committed `keyboard_config_21000009.json` fixture,
|
(`tests/AcDream.App.Tests/UI/Layout/KeyboardConfigLiveMountProbeTests.cs`,
|
||||||
so the structural conformance suite cannot see whatever the live
|
`ACDREAM_PROBE_LIVE_MOUNT=1` — the #372-class fixture suite was green
|
||||||
DAT mount does differently (string resolution, template sizing,
|
throughout, again):
|
||||||
anchor baselines, window extent). Diagnosis must start from live-mount
|
|
||||||
evidence, not the fixture: extend the `ACDREAM_PROBE_LIVE_MOUNT=1` probe
|
|
||||||
(`OptionsPanelLiveMountProbeTests` pattern) to mount `0x21000009` against
|
|
||||||
the real DATs and dump per-element rect + resolved caption, then compare
|
|
||||||
against the user's four observations. Candidate families (to CONFIRM, not
|
|
||||||
assume): caption lookups missing their real string table (the exact
|
|
||||||
`0x2300000D` class from #372's session), row-template text elements
|
|
||||||
sized/positioned from degenerate baselines, and the screen's authored
|
|
||||||
extent vs where children actually land.
|
|
||||||
|
|
||||||
**Blocks the OP8 connected gate.**
|
1. **The missing string resolver** ("buttons lacked text" + "no text in
|
||||||
|
tabs"): `RetailUiRuntime.MountKeyboardConfig`'s main
|
||||||
|
`LayoutImporter.Build` call was the ONE mount in that class not
|
||||||
|
passing `strings.Resolve` — every AUTHORED caption (OK / Cancel /
|
||||||
|
Defaults / Revert / Load File... / Save As..., the six ActionClass
|
||||||
|
tab labels, the Command / Mapping 1-3 column headers) built empty,
|
||||||
|
while the controller's own `resolveString` lookups (row captions)
|
||||||
|
worked, which is why the screen was recognizable but textless. The
|
||||||
|
probe builds the same layout both ways: resolver-less = every caption
|
||||||
|
`''`; with resolver = `Movement/Camera/Combat/UI/CharacterSettings/
|
||||||
|
Emotes`, `Command`, `Mapping 1/2/3`, `OK`, `Cancel`, ... Fix: pass
|
||||||
|
the resolver, like every sibling mount.
|
||||||
|
2. **Parked row-template prototypes** ("buttons outside the window" +
|
||||||
|
"overlapping text"): `gmKeyboardUI` (`0x21000009`) authors its
|
||||||
|
ListBox row templates — the header text `0x1000002E` and the action
|
||||||
|
row `0x1000002F` carrying the three 100x32 key buttons — as ordinary
|
||||||
|
TOP-LEVEL siblings of the screen, referenced by dat property `0x64`
|
||||||
|
(the template list). Retail never instantiates template-list elements
|
||||||
|
as live widgets (`AddItemFromTemplateList` clones from the desc — the
|
||||||
|
same re-import our `UiTemplateListBox.TemplateResolver` performs), but
|
||||||
|
`LayoutImporter.ImportInfos` built them as live elements parked at the
|
||||||
|
screen's (0,0): three key buttons at screen top y=0..32, ABOVE the
|
||||||
|
framed panel (which starts at y=62) — the "outside the window"
|
||||||
|
buttons — with the 570x40 header text overlapping them and the
|
||||||
|
window's top chrome. Fix: `ImportInfos(dats, layoutId)` now skips
|
||||||
|
top-level elements referenced by a SAME-LAYOUT template list (the
|
||||||
|
same skip class as the existing BaseElement-prototype filter;
|
||||||
|
same-layout only, because element ids collide across layouts —
|
||||||
|
`0x10000211` is a page in BOTH the options and keyboard layouts).
|
||||||
|
Post-fix the import collapses to the framed 600x476 panel with every
|
||||||
|
screen button inside its bounds.
|
||||||
|
|
||||||
|
**Blocks the OP8 connected gate** — re-gate §OP8 after this commit.
|
||||||
|
|
||||||
## #374 — Config tab: picking a new Resolution does not resize the window (live gate failure)
|
## #374 — Config tab: picking a new Resolution does not resize the window (live gate failure)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -869,6 +869,17 @@ Three live runs against local ACE (`127.0.0.1:9000`, `+Acdream`,
|
||||||
|
|
||||||
## OP8 — Configure Keyboard
|
## OP8 — Configure Keyboard
|
||||||
|
|
||||||
|
> **Gate-2 re-test note (#375):** the first look at this screen was a
|
||||||
|
> visual mess — textless buttons/tabs, stray buttons above the window,
|
||||||
|
> overlapping text. Two root causes, both fixed: the screen's build was
|
||||||
|
> missing its string resolver (every authored caption rendered empty),
|
||||||
|
> and the row-template prototypes (a header + a three-button row) were
|
||||||
|
> being built as live widgets parked at the screen's top-left. Expect
|
||||||
|
> now: captioned tabs (Movement/Camera/Combat/UI/CharacterSettings/
|
||||||
|
> Emotes), captioned buttons (Defaults/Revert/OK/Cancel, Load File.../
|
||||||
|
> Save As...), Command + Mapping 1-3 column headers, and NOTHING
|
||||||
|
> rendered above or outside the framed panel.
|
||||||
|
|
||||||
**Known, tracked behaviors — do NOT file as defects (read before testing):**
|
**Known, tracked behaviors — do NOT file as defects (read before testing):**
|
||||||
|
|
||||||
- **Shared combat keys prompt a false conflict (ISSUES #373).** Retail
|
- **Shared combat keys prompt a false conflict (ISSUES #373).** Retail
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,35 @@ public static class LayoutImporter
|
||||||
tops.Add(Resolve(dats, d, new HashSet<(uint, uint)>()));
|
tops.Add(Resolve(dats, d, new HashSet<(uint, uint)>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #375: a Type-5 ListBox's row-template list (dat property 0x64) can
|
||||||
|
// name SAME-LAYOUT elements as its row prototypes — gmKeyboardUI
|
||||||
|
// (0x21000009) authors its header (0x1000002E) and action-row
|
||||||
|
// (0x1000002F, the three key buttons) templates as ordinary top-level
|
||||||
|
// siblings of the screen. Retail never instantiates a template-list
|
||||||
|
// element as a live widget (AddItemFromTemplateList clones from the
|
||||||
|
// desc on demand — the SAME re-import our UiTemplateListBox's
|
||||||
|
// TemplateResolver performs), so building them here parked two live
|
||||||
|
// prototype rows at the screen's (0,0), over and outside the framed
|
||||||
|
// panel. Same skip class as the BaseElement prototypes above, keyed on
|
||||||
|
// the template-list reference instead. Same-LAYOUT references only:
|
||||||
|
// element ids collide across layouts (0x10000211 is a page in BOTH the
|
||||||
|
// options and keyboard layouts), so a cross-layout entry must never
|
||||||
|
// suppress a coincidentally-same-id element here.
|
||||||
|
var referencedAsTemplate = new HashSet<uint>();
|
||||||
|
foreach (ElementInfo top in tops)
|
||||||
|
CollectTemplateRefs(top, layoutId, referencedAsTemplate);
|
||||||
|
if (referencedAsTemplate.Count > 0)
|
||||||
|
{
|
||||||
|
for (int i = tops.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (!referencedAsTemplate.Contains(tops[i].Id)) continue;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[D.2b] LayoutImporter: skipping row-template element 0x{tops[i].Id:X8} "
|
||||||
|
+ $"in layout 0x{layoutId:X8} (referenced by a same-layout template list).");
|
||||||
|
tops.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tops.Count == 1)
|
if (tops.Count == 1)
|
||||||
return tops[0];
|
return tops[0];
|
||||||
|
|
||||||
|
|
@ -235,6 +264,21 @@ public static class LayoutImporter
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Recursively gathers every SAME-LAYOUT element id referenced by a
|
||||||
|
/// row-template list (dat property 0x64) anywhere in <paramref name="info"/>'s
|
||||||
|
/// resolved subtree — the skip set for the #375 parked-prototype filter above.</summary>
|
||||||
|
private static void CollectTemplateRefs(
|
||||||
|
ElementInfo info, uint layoutId, HashSet<uint> referenced)
|
||||||
|
{
|
||||||
|
foreach (UiTemplateListEntry entry in info.TemplateList)
|
||||||
|
{
|
||||||
|
if (entry.TemplateLayoutId == layoutId)
|
||||||
|
referenced.Add(entry.TemplateElementId);
|
||||||
|
}
|
||||||
|
foreach (ElementInfo child in info.Children)
|
||||||
|
CollectTemplateRefs(child, layoutId, referenced);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail <c>UIElementManager::CreateRootElementByDataID</c> counterpart: resolve one
|
/// Retail <c>UIElementManager::CreateRootElementByDataID</c> counterpart: resolve one
|
||||||
/// authored root from a catalog-style LayoutDesc instead of instantiating every
|
/// authored root from a catalog-style LayoutDesc instead of instantiating every
|
||||||
|
|
|
||||||
|
|
@ -2283,15 +2283,23 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, Layout.KeyboardConfigController.LayoutId);
|
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, Layout.KeyboardConfigController.LayoutId);
|
||||||
|
strings = new DatStringResolver(_bindings.Assets.Dats);
|
||||||
layout = info is null
|
layout = info is null
|
||||||
? null
|
? null
|
||||||
: LayoutImporter.Build(
|
: LayoutImporter.Build(
|
||||||
info,
|
info,
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont);
|
_bindings.Assets.ResolveFont,
|
||||||
|
// #375: the main screen build was the ONE mount in this class
|
||||||
|
// missing the string resolver — every AUTHORED caption
|
||||||
|
// (OK/Cancel/Defaults/Revert/Load/Save, the six ActionClass
|
||||||
|
// tab labels, the Command/Mapping column headers) rendered
|
||||||
|
// empty at the gate, while the controller's own resolveString
|
||||||
|
// lookups (row captions, headers) worked. Keep in step with
|
||||||
|
// every sibling Mount* Build call above.
|
||||||
|
strings.Resolve);
|
||||||
snapshot = RetailActionMapReader.Read(_bindings.Assets.Dats);
|
snapshot = RetailActionMapReader.Read(_bindings.Assets.Dats);
|
||||||
strings = new DatStringResolver(_bindings.Assets.Dats);
|
|
||||||
}
|
}
|
||||||
if (layout is null || snapshot is null)
|
if (layout is null || snapshot is null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
using System.IO;
|
||||||
|
using AcDream.App.UI;
|
||||||
|
using AcDream.App.UI.Layout;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TEMPORARY #375 gate-failure probe (Campaign OP connected gate 2,
|
||||||
|
/// 2026-08-11): the user found the Configure Keyboard screen a visual mess —
|
||||||
|
/// missing button/tab captions, buttons outside the window, overlapping text —
|
||||||
|
/// while KeyboardConfigControllerTests stays green against the committed
|
||||||
|
/// fixture (the #372 structural-false-negative class again). This probe runs
|
||||||
|
/// the PRODUCTION mount path against the live DATs and dumps per-element
|
||||||
|
/// rect + caption evidence for each observation. Env-gated like the other
|
||||||
|
/// live probes so CI/dat-less runs skip it.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class KeyboardConfigLiveMountProbeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProbeKeyboardLiveMount()
|
||||||
|
{
|
||||||
|
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
|
||||||
|
return;
|
||||||
|
|
||||||
|
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||||
|
?? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Documents",
|
||||||
|
"Asheron's Call");
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
var strings = new DatStringResolver(dats);
|
||||||
|
|
||||||
|
ElementInfo? info = LayoutImporter.ImportInfos(
|
||||||
|
dats, KeyboardConfigController.LayoutId);
|
||||||
|
Assert.NotNull(info);
|
||||||
|
|
||||||
|
// Production shape A — what MountKeyboardConfig actually built at the
|
||||||
|
// gate (NO string resolver on the main Build; the template build DOES
|
||||||
|
// pass one — the asymmetry under investigation).
|
||||||
|
ImportedLayout withoutStrings = LayoutImporter.Build(
|
||||||
|
info!, _ => (0u, 0, 0), null);
|
||||||
|
// Shape B — the same build WITH the resolver every sibling mount passes.
|
||||||
|
ImportedLayout withStrings = LayoutImporter.Build(
|
||||||
|
info!, _ => (0u, 0, 0), null, null, strings.Resolve);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] layout root id=0x{info!.Id:X8} rect=({withoutStrings.Root.Left},{withoutStrings.Root.Top} "
|
||||||
|
+ $"{withoutStrings.Root.Width}x{withoutStrings.Root.Height}) children={withoutStrings.Root.Children.Count}");
|
||||||
|
|
||||||
|
// The window-vs-content geometry question (observation 2): the layout
|
||||||
|
// is an 800x600 retail full-SCREEN — which child is the visible framed
|
||||||
|
// panel, and what escapes ITS rect?
|
||||||
|
DumpTreeRects(withoutStrings.Root, 0, maxDepth: 3);
|
||||||
|
UiElement? windowRoot = withoutStrings.FindElement(
|
||||||
|
KeyboardConfigController.WindowRootElementId);
|
||||||
|
Console.WriteLine(windowRoot is null
|
||||||
|
? "[kbprobe] window root 0x1000001F: MISSING from flat index"
|
||||||
|
: $"[kbprobe] window root 0x1000001F rect=({windowRoot.Left},{windowRoot.Top} "
|
||||||
|
+ $"{windowRoot.Width}x{windowRoot.Height}) children={windowRoot.Children.Count} "
|
||||||
|
+ $"type={windowRoot.GetType().Name}");
|
||||||
|
|
||||||
|
// Observation 1+3 — "buttons lacked descriptive text", "no text in
|
||||||
|
// tabs": dump every UiButton/UiText caption in BOTH shapes.
|
||||||
|
DumpCaptions("without-strings", withoutStrings.Root);
|
||||||
|
DumpCaptions("with-strings", withStrings.Root);
|
||||||
|
|
||||||
|
// Observation 2 — "some buttons were outside of the window": every
|
||||||
|
// element whose screen rect escapes the root's own extent.
|
||||||
|
DumpOutOfBounds(withoutStrings.Root);
|
||||||
|
|
||||||
|
// #375 regression pins (live-DAT, env-gated — the fix evidence):
|
||||||
|
// (a) the same-layout row-template prototypes (0x1000002E header,
|
||||||
|
// 0x1000002F action row) must NOT build as live elements —
|
||||||
|
// the parked copies were the "buttons outside the window" /
|
||||||
|
// "overlapping text" halves of the gate report.
|
||||||
|
Assert.Null(withStrings.FindElement(0x1000002Eu));
|
||||||
|
Assert.Null(withStrings.FindElement(0x1000002Fu));
|
||||||
|
// (b) the resolver-passing build resolves the screen's authored
|
||||||
|
// captions — the "buttons/tabs lacked text" halves. (Production
|
||||||
|
// now passes the resolver; the without-strings shape above is
|
||||||
|
// kept only as the delta record.)
|
||||||
|
foreach ((uint id, string expected) in new[]
|
||||||
|
{
|
||||||
|
(0x1000002Au, "Defaults"),
|
||||||
|
(0x1000002Bu, "Revert"),
|
||||||
|
(0x1000002Cu, "OK"),
|
||||||
|
(0x1000002Du, "Cancel"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
UiElement? el = withStrings.FindElement(id);
|
||||||
|
UiButton button = Assert.IsType<UiButton>(el);
|
||||||
|
Assert.Equal(expected, button.Label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observation 4 — "text next to the buttons was overlapping": the row
|
||||||
|
// template's authored geometry vs the synthesized 260px caption column.
|
||||||
|
foreach ((string name, uint pageId) in new[]
|
||||||
|
{
|
||||||
|
("Movement", 0x1000049Du),
|
||||||
|
("Camera", 0x1000049Fu),
|
||||||
|
("Combat", 0x100004A1u),
|
||||||
|
("UI", 0x100004A3u),
|
||||||
|
("CharacterSettings", 0x10000211u),
|
||||||
|
("Emote", 0x100004A5u),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
UiElement? page = UiElement.FindDescendant(withStrings.Root, pageId);
|
||||||
|
UiElement? lb = page is null ? null : UiElement.FindDescendant(page, 0x10000025u);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] page {name} 0x{pageId:X8} -> {(page is null ? "MISSING" : $"({page.Left},{page.Top} {page.Width}x{page.Height})")} "
|
||||||
|
+ $"listbox -> {(lb is null ? "MISSING" : $"{lb.GetType().Name} ({lb.Left},{lb.Top} {lb.Width}x{lb.Height})")}");
|
||||||
|
if (lb is UiTemplateListBox tlb)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < tlb.Templates.Count; i++)
|
||||||
|
{
|
||||||
|
(uint layoutId, uint elementId) =
|
||||||
|
(tlb.Templates[i].TemplateLayoutId, tlb.Templates[i].TemplateElementId);
|
||||||
|
ElementInfo? tInfo = LayoutImporter.ImportInfos(dats, layoutId, elementId);
|
||||||
|
if (tInfo is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[kbprobe] template[{i}] 0x{layoutId:X8}/0x{elementId:X8} -> IMPORT MISSING");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UiElement built = LayoutImporter.Build(
|
||||||
|
tInfo, _ => (0u, 0, 0), null, null, strings.Resolve).Root;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] template[{i}] 0x{layoutId:X8}/0x{elementId:X8} -> {built.GetType().Name} "
|
||||||
|
+ $"({built.Left},{built.Top} {built.Width}x{built.Height}) children={built.Children.Count}");
|
||||||
|
foreach (uint keyBtn in new[] { 0x10000030u, 0x10000031u, 0x10000032u })
|
||||||
|
{
|
||||||
|
UiElement? b = UiElement.FindDescendant(built, keyBtn);
|
||||||
|
if (b is not null)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] key-button 0x{keyBtn:X8} ({b.Left},{b.Top} {b.Width}x{b.Height})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only the first page's templates matter for geometry (all six share
|
||||||
|
// the same authored list) — stop after one full dump.
|
||||||
|
if (lb is not null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpCaptions(string tag, UiElement root)
|
||||||
|
{
|
||||||
|
Walk(root, el =>
|
||||||
|
{
|
||||||
|
string? caption = el switch
|
||||||
|
{
|
||||||
|
UiButton b => b.Label,
|
||||||
|
UiText t => string.Join(
|
||||||
|
" / ", t.LinesProvider().Select(static l => l.Text)),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (el is UiButton or UiText)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] {tag} 0x{el.EventId:X8} {el.GetType().Name} "
|
||||||
|
+ $"({el.Left},{el.Top} {el.Width}x{el.Height}) caption='{caption ?? "<null>"}'");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpOutOfBounds(UiElement root)
|
||||||
|
{
|
||||||
|
Walk(root, el =>
|
||||||
|
{
|
||||||
|
var p = el.ScreenPosition;
|
||||||
|
bool outside = p.X < root.Left - 0.5f || p.Y < root.Top - 0.5f
|
||||||
|
|| p.X + el.Width > root.Left + root.Width + 0.5f
|
||||||
|
|| p.Y + el.Height > root.Top + root.Height + 0.5f;
|
||||||
|
if (outside)
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] OUT-OF-BOUNDS 0x{el.EventId:X8} {el.GetType().Name} "
|
||||||
|
+ $"screen=({p.X},{p.Y} {el.Width}x{el.Height}) rootExtent={root.Width}x{root.Height}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Walk(UiElement el, Action<UiElement> visit)
|
||||||
|
{
|
||||||
|
visit(el);
|
||||||
|
foreach (UiElement c in el.Children)
|
||||||
|
Walk(c, visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpTreeRects(UiElement el, int depth, int maxDepth)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[kbprobe] tree {new string(' ', depth * 2)}{el.GetType().Name} "
|
||||||
|
+ $"({el.Left},{el.Top} {el.Width}x{el.Height}) children={el.Children.Count}");
|
||||||
|
if (depth >= maxDepth) return;
|
||||||
|
foreach (UiElement c in el.Children)
|
||||||
|
DumpTreeRects(c, depth + 1, maxDepth);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue