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
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:
parent
1688863366
commit
d674b99f56
5 changed files with 113 additions and 28 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue