feat(ui): double-click-to-buy (AP-171, user-approved) + #353 toolbar text fixes — authored right-justify and two-line name wrap (Fable)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run

Double-clicking a vendor shop item now buys through the Buy button's
exact quantity/price path — retail has NO double-click-to-buy (the
named table sweep's negative evidence stands); the user chose the
addition explicitly and AP-171 records it.

#353 (pre-existing, user-reported): the stack-count entry is AUTHORED
HJustify=2 — right-justified flush against the slider on its own row —
and UiField already supported RightAligned; nobody had honored the
authored value. The name element is AUTHORED two lines tall (H=31,
W=140): long names now word-wrap at the authored pixel width onto a
second centered row via two stacked one-line labels reusing the
existing centered draw path (WrapNameTwoLines: greedy word break, no
hyphenation, second row clips like retail).

Ten SelectedObjectController structure tests updated from
single-label to first-label access. Lesson re-learned the hard way:
the first "green" run used a stale TEST assembly (only the App
project had been rebuilt) — the clean-room caught it, per
feedback_stale_build_artifacts. Full App 4,329/3 and Core 4,381/1
verified green on properly rebuilt assemblies; the one transient
Core Release failure did not reproduce and is noted on #351.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-08 17:42:02 +02:00
parent 1688863366
commit d674b99f56
5 changed files with 113 additions and 28 deletions

View file

@ -176,6 +176,11 @@ public sealed class SelectedObjectController : IRetainedPanelController
if (_stackSizeEntry is not null)
{
_stackSizeEntry.Visible = false;
// #353: the entry is AUTHORED HJustify=2 (right) at X=0 W=50,
// flush against the slider at X=50 on the same row — the count
// reads right-adjacent to the bar, retail's look. UiField
// already supports it; the importer does not carry HJustify.
_stackSizeEntry.RightAligned = true;
_stackSizeEntry.Selectable = true;
_stackSizeEntry.ClearOnSubmit = false;
_stackSizeEntry.RecordHistory = false;
@ -205,26 +210,46 @@ public sealed class SelectedObjectController : IRetainedPanelController
if (_name is not null)
{
_name.ZOrder = NameZOrderOnTop;
var label = new UiText
// #353: the name element is AUTHORED two lines tall (H=31 at
// W=140) — a long name wraps at the authored PIXEL width onto a
// second row instead of overflowing (user-verified retail
// behavior). Two stacked centered one-line labels reuse the
// existing centered draw path; the second draws nothing when
// the name fits.
float nameWidth = _name.Width;
var wrapFont = datFont;
Func<int, UiText.Line[]> lineFor = index =>
{
Left = 0f, Top = 0f, Width = _name.Width, Height = NameBandHeight,
Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
Centered = true,
OneLine = true,
DatFont = datFont,
ClickThrough = true,
AcceptsFocus = false,
IsEditControl = false,
CapturesPointerDrag = false,
LinesProvider = () =>
{
var n = _currentName;
return string.IsNullOrEmpty(n)
? Array.Empty<UiText.Line>()
: new[] { new UiText.Line(n, NameColor) };
},
var n = _currentName;
if (string.IsNullOrEmpty(n))
return Array.Empty<UiText.Line>();
(string first, string second) = WrapNameTwoLines(n, nameWidth, wrapFont);
string text = index == 0 ? first : second;
return text.Length == 0
? Array.Empty<UiText.Line>()
: new[] { new UiText.Line(text, NameColor) };
};
_name.AddChild(label);
for (int lineIndex = 0; lineIndex < 2; lineIndex++)
{
int captured = lineIndex;
var label = new UiText
{
Left = 0f,
Top = captured * NameBandHeight,
Width = _name.Width,
Height = NameBandHeight,
Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right,
Centered = true,
OneLine = true,
DatFont = datFont,
ClickThrough = true,
AcceptsFocus = false,
IsEditControl = false,
CapturesPointerDrag = false,
LinesProvider = () => lineFor(captured),
};
_name.AddChild(label);
}
}
// Register the handlers LAST so the initial state is fully set up first.
@ -454,6 +479,37 @@ public sealed class SelectedObjectController : IRetainedPanelController
_manaMeter.Visible = true;
}
/// <summary>
/// #353: greedy word wrap of the selected-object name into at most two
/// lines at the authored pixel width. A single word longer than the
/// width stays unbroken on its line (retail does not hyphenate). The
/// second line carries everything remaining — the authored element is
/// exactly two lines tall, so anything longer simply clips like retail.
/// </summary>
internal static (string First, string Second) WrapNameTwoLines(
string name,
float width,
UiDatFont? font)
{
if (font is null || font.MeasureWidth(name) <= width)
return (name, string.Empty);
int breakAt = -1;
for (int i = 0; i < name.Length; i++)
{
if (name[i] != ' ')
continue;
if (font.MeasureWidth(name[..i]) <= width)
breakAt = i;
else
break;
}
if (breakAt <= 0)
return (name, string.Empty);
return (name[..breakAt], name[(breakAt + 1)..].TrimStart());
}
private void CommitStackEntry(string text)
=> _splitQuantity.SetFromText(text);

View file

@ -1107,6 +1107,19 @@ public sealed class VendorUiController : IRetainedPanelController, IItemListDrag
cell.Selected = item.ItemGuid == selectedGuid;
VendorShopItem captured = item;
cell.Clicked = () => _selection.Select(captured.ItemGuid, SelectionChangeSource.Vendor);
// AP-171: double-click buys the item — a DELIBERATE,
// user-approved modernization. Retail has NO
// double-click-to-buy anywhere in the named function
// table (negative evidence recorded at the Slice 6
// research); the user requested it explicitly
// 2026-08-08 after being told so. Select-then-buy so
// the quantity/price path is identical to the Buy
// button's.
cell.DoubleClicked = () =>
{
_selection.Select(captured.ItemGuid, SelectionChangeSource.Vendor);
BuySelectedItem();
};
_itemList.AddItem(cell);
}
}