fix: complete retail parity stability pass
All checks were successful
CI / linux-portable (push) Successful in 3m41s
CI / windows-gate (push) Successful in 6m49s
CI / release (push) Successful in 3m22s

This commit is contained in:
Erik 2026-08-28 20:01:39 +02:00
parent d3df4cb20a
commit f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions

View file

@ -85,21 +85,20 @@ public sealed class UiMenu : UiElement
public float ColumnWidth { get; set; } = 191f; // dat item template W=191
/// <summary>
/// G5 (vendor gate finding): retail's authored vendor category popup
/// Retail's authored vendor category popup
/// (LayoutDesc <c>0x21000043</c>, root <c>0x1000034F</c>) pairs its
/// ListBox (element <c>0x10000350</c>, type <c>0x5</c>) with a SIBLING
/// <c>UIElement_Scrollbar</c> (element <c>0x10000351</c>, type <c>0xB</c>,
/// 16px wide, docked immediately right of the list at x=100) — verified
/// via a live-dat scan (<c>tools/VendorLayoutScan</c>) against
/// <c>client_local_English.dat</c>: the ListBox reads a single-column
/// shape (attributes resolving to <c>m_nCols=1</c>/<c>m_nRows=6</c>) and
/// the row template (<c>0x10000352</c>) is 100×18 — a SCROLLABLE single
/// column with 6 visible rows, not our earlier column-major grid
/// approximation (which showed all 18 categories at once across 3
/// columns, never matching the retail screenshot's ~one-column-with-
/// scrollbar look). <see cref="RowsPerColumn"/> becomes the VISIBLE ROW
/// COUNT in this mode (still authored-driven — 108px ListBox height / 18px
/// row height = 6). Chat's own popup (LayoutDesc <c>0x21000006</c>) has
/// via installed-DAT inspection against <c>client_local_English.dat</c>.
/// The authored 100x108 ListBox starts with a six-row viewport, but retail
/// <c>UIElement_ListBox::UpdateLayout @0x0046e460</c> resizes its scrollable
/// content to the number of inserted categories. Because all four edges are
/// docked, message <c>0x32</c> reaches
/// <c>UIElement_Menu::RecalculatePopupSize @0x0046caf0</c> and grows or
/// shrinks the popup to that content. The sibling scrollbar authors property
/// <c>0x79=true</c>, so it disappears once the resized viewport fits the
/// content. Chat's own popup (LayoutDesc <c>0x21000006</c>) has
/// NO sibling scrollbar element and keeps the class default false — the
/// legacy column-major grid path below is untouched for it.
/// </summary>
@ -140,6 +139,24 @@ public sealed class UiMenu : UiElement
public uint ScrollUpSprite { get; set; }
public uint ScrollDownSprite { get; set; }
/// <summary>
/// Retail scrollbar property <c>0x79</c> for the popup's authored sibling
/// scrollbar. When the content fits and the scrollbar is therefore disabled,
/// hide the sibling completely: no chrome, pointer target, or reserved width.
/// Vendor popup element <c>0x10000351</c> authors this true. Retail owns a real
/// sibling widget and calls <c>SetVisible(false)</c> from
/// <c>UIElement_Scrollbar::UpdateLayout @0x004710d0</c>; this procedural popup
/// must also remove the sibling's width from its flattened geometry to produce
/// the same visible result.
/// </summary>
public bool PopupScrollbarHideWhenDisabled { get; set; }
/// <summary>Presentation projection shared by drawing and pointer dispatch.
/// Internal for the same focused-test purpose as
/// <see cref="UiScrollbar.IsPresentationVisible"/>.</summary>
internal bool IsPopupScrollbarPresentationVisible
=> !PopupScrollbarHideWhenDisabled || PopupContentOverflows;
private bool _draggingPopupThumb;
private float _popupThumbDragOffset;
@ -252,9 +269,8 @@ public sealed class UiMenu : UiElement
/// popup ListBox (0x21000043/0x10000358) reads edges L=T=R=B=1
/// (menuprobe3, <c>OptionsPanelLiveMountProbeTests</c>), so
/// <see cref="AcDream.App.UI.Layout.ConfigOptionsPageController"/> sets
/// this true; chat's grid popup and vendor's shipped 6-row window keep
/// the class default false (vendor's authored ListBox is ALSO docked —
/// tracked as its own issue, not silently reworked here).
/// this true. Vendor's ListBox has the same four docked edges and therefore
/// enables it too; chat's grid popup keeps the class default false.
/// When set, <see cref="RowsPerColumn"/> stops being the visible-window
/// height and the popup shows every item with no scroll overflow.
/// </summary>
@ -333,12 +349,14 @@ public sealed class UiMenu : UiElement
// Interior = the row content; Outer = interior + the 8-piece bevel ring.
// Scrollable: always exactly one column (RowsPerColumn is the VISIBLE window,
// not a wrap threshold), widened by the docked scrollbar's own authored width.
// not a wrap threshold), widened by the docked scrollbar only while that
// sibling is presentation-visible. Retail property 0x79 removes a disabled
// scrollbar completely; keeping its width caused #386's empty placeholder.
private int ColumnCount => Scrollable
? 1
: (Items.Count + RowsPerColumn - 1) / System.Math.Max(1, RowsPerColumn);
private float InteriorW => Scrollable
? ColumnWidth + ScrollbarWidth
? ColumnWidth + EffectiveScrollbarWidth
: ColumnCount * ColumnWidth;
/// <summary>The popup's visible row count. Size-to-content (retail's
@ -352,6 +370,15 @@ public sealed class UiMenu : UiElement
? System.Math.Max(1, Items.Count)
: RowsPerColumn;
/// <summary>UiMenu rows have a fixed authored height, so this is the same
/// overflow decision <see cref="ConfigurePopupScroll"/> publishes to
/// <see cref="PopupScroll"/>, but is stable before the first draw/event has
/// configured that model.</summary>
private bool PopupContentOverflows => Items.Count > EffectiveVisibleRows;
private float EffectiveScrollbarWidth
=> IsPopupScrollbarPresentationVisible ? ScrollbarWidth : 0f;
private float InteriorH => EffectiveVisibleRows * RowHeight;
private float OuterW => InteriorW + 2 * Border;
private float OuterH => InteriorH + 2 * Border;
@ -362,6 +389,11 @@ public sealed class UiMenu : UiElement
/// a full render pass.</summary>
public float PopupOuterHeight => OuterH;
/// <summary>The popup's outer (bevel-inclusive) width. Exposed alongside
/// <see cref="PopupOuterHeight"/> so the retail hide-disabled scrollbar rule
/// can be pinned without a GPU render harness.</summary>
public float PopupOuterWidth => OuterW;
/// <summary>
/// G7 (vendor gate finding, item 2 — popup direction): port of retail
/// <c>UIElement_Menu::Open</c> (pc:120210-120252, <c>0x0046cc30</c>)'s Y placement:
@ -558,10 +590,11 @@ public sealed class UiMenu : UiElement
/// <summary>
/// G5: single-column popup with a docked scrollbar — port of the vendor category
/// dropdown's authored shape (LayoutDesc <c>0x21000043</c>, see <see cref="Scrollable"/>'s
/// doc comment). Draws exactly <see cref="RowsPerColumn"/> rows (the authored visible
/// window), sliced from <see cref="Items"/> starting at <see cref="VisibleTopRow"/>, plus
/// the scrollbar chrome using the SAME thumb geometry <see cref="UiScrollbar"/> itself
/// uses (<see cref="UiScrollbar.ThumbRect"/>).
/// doc comment). Draws <see cref="EffectiveVisibleRows"/> rows, sliced from
/// <see cref="Items"/> starting at <see cref="VisibleTopRow"/>, plus the scrollbar
/// chrome when its authored disabled-presentation rule allows it. Thumb geometry
/// is the same helper <see cref="UiScrollbar"/> itself uses
/// (<see cref="UiScrollbar.ThumbRect"/>).
/// </summary>
private void DrawScrollablePopup(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve)
{
@ -602,9 +635,9 @@ public sealed class UiMenu : UiElement
{
int lineHeight = System.Math.Max(1, (int)MathF.Round(RowHeight));
PopupScroll.LineHeight = lineHeight;
// Size-to-content: view == content, so HasOverflow is false and the
// scrollbar draws its chrome with no thumb (retail's authored
// scrollbar sibling stretches with the docked popup the same way).
// Size-to-content: view == content, so HasOverflow is false. Whether
// the disabled scrollbar remains visible is its authored 0x79 property,
// projected by IsPopupScrollbarPresentationVisible.
PopupScroll.SetExtents(Items.Count * lineHeight, EffectiveVisibleRows * lineHeight);
}
@ -625,6 +658,8 @@ public sealed class UiMenu : UiElement
private void DrawPopupScrollbar(
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve, float x, float y)
{
if (!IsPopupScrollbarPresentationVisible) return;
DrawSprite(ctx, resolve, ScrollTrackSprite, x, y, ScrollbarWidth, InteriorH);
float decExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH);
@ -801,7 +836,9 @@ public sealed class UiMenu : UiElement
}
float scrollbarX = ColumnWidth;
if (ix >= scrollbarX && ix < scrollbarX + ScrollbarWidth && iy >= 0 && iy < InteriorH)
if (IsPopupScrollbarPresentationVisible
&& ix >= scrollbarX && ix < scrollbarX + ScrollbarWidth
&& iy >= 0 && iy < InteriorH)
{
ConfigurePopupScroll();
float decExtent = System.Math.Clamp(ScrollButtonExtent, 0f, InteriorH);