feat(ui): port retail selected-stack quantity

Bind the authored stack count entry and horizontal slider to one Core split-quantity owner, preserve retail count-first naming and exact 1000-step rounding, refresh on stack changes, and consume the selected amount during merges. Conformance covers the production DAT fixture and retained pointer/focus paths.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 10:07:25 +02:00
parent dc1649c493
commit a20e5c68c7
19 changed files with 595 additions and 51 deletions

View file

@ -33,6 +33,10 @@ public sealed class UiField : UiElement
public int MaxCharacters { get; set; } = 0xFFFF;
public bool OneLine { get; set; } = true;
public bool Selectable { get; set; }
public bool ClearOnSubmit { get; set; } = true;
public bool RecordHistory { get; set; } = true;
public Func<char, bool>? CharacterFilter { get; set; }
public bool SelectAllOnFocus { get; set; }
/// <summary>Keyboard device for clipboard (Ctrl+C/X/V) + modifier state (Ctrl/Shift).
/// Wired by the host from <see cref="UiHost.Keyboard"/>.</summary>
@ -48,6 +52,8 @@ public sealed class UiField : UiElement
public uint FocusFieldSprite { get; set; }
public Action<string>? OnSubmit { get; set; }
public Action? OnFocusGained { get; set; }
public Action<string>? OnFocusLost { get; set; }
private string _text = "";
private int _caret;
@ -61,6 +67,7 @@ public sealed class UiField : UiElement
private bool _focused;
private bool _selecting; // mouse drag in progress
private bool _preserveFocusSelectionOnMouseDown;
private float _scrollX; // horizontal pixel scroll so the caret stays in the field
// Held-key auto-repeat (Silk delivers one KeyDown per physical press).
@ -84,7 +91,7 @@ public sealed class UiField : UiElement
public void InsertChar(char c)
{
if (c < 0x20 || c == 0x7F) return;
if (c < 0x20 || c == 0x7F || (CharacterFilter is not null && !CharacterFilter(c))) return;
DeleteSelection();
if (_text.Length >= MaxCharacters) return;
_text = _text.Insert(_caret, c.ToString());
@ -149,13 +156,23 @@ public sealed class UiField : UiElement
return true;
}
private void SelectAll()
public void SelectAllText()
{
if (_text.Length == 0) { _selAnchor = null; return; }
_selAnchor = 0;
_caret = _text.Length;
}
public void SetText(string? text)
{
_text = text ?? "";
if (_text.Length > MaxCharacters)
_text = _text[..MaxCharacters];
_caret = _text.Length;
_selAnchor = null;
_historyIndex = -1;
}
private void CopySelection()
{
var s = SelectedText();
@ -179,7 +196,8 @@ public sealed class UiField : UiElement
// Single-line field: strip control chars (newlines/tabs) from pasted text.
var sb = new System.Text.StringBuilder(clip.Length);
foreach (char ch in clip)
if (ch >= 0x20 && ch != 0x7F) sb.Append(ch);
if (ch >= 0x20 && ch != 0x7F
&& (CharacterFilter is null || CharacterFilter(ch))) sb.Append(ch);
if (sb.Length == 0) return;
DeleteSelection();
@ -196,10 +214,14 @@ public sealed class UiField : UiElement
public void Submit()
{
var t = _text;
if (t.Trim().Length == 0) { Clear(); return; }
if (t.Trim().Length == 0)
{
if (ClearOnSubmit) Clear();
return;
}
OnSubmit?.Invoke(t);
PushHistory(t);
Clear();
if (RecordHistory) PushHistory(t);
if (ClearOnSubmit) Clear();
}
private void Clear() { _text = ""; _caret = 0; _selAnchor = null; _historyIndex = -1; }
@ -370,10 +392,23 @@ public sealed class UiField : UiElement
{
switch (e.Type)
{
case UiEventType.FocusGained: _focused = true; return true;
case UiEventType.FocusGained:
_focused = true;
if (SelectAllOnFocus)
{
SelectAllText();
// UiRoot assigns focus before delivering the initiating MouseDown.
// Retail's activation notice selects all after that press, so preserve
// this selection across the one MouseDown that caused focus.
_preserveFocusSelectionOnMouseDown = true;
}
OnFocusGained?.Invoke();
return true;
case UiEventType.FocusLost:
OnFocusLost?.Invoke(_text);
_focused = false; _historyIndex = -1;
_selAnchor = null; _selecting = false; _repeatKey = null;
_preserveFocusSelectionOnMouseDown = false;
return true;
case UiEventType.Char:
@ -381,6 +416,12 @@ public sealed class UiField : UiElement
return true;
case UiEventType.MouseDown:
if (_preserveFocusSelectionOnMouseDown)
{
_preserveFocusSelectionOnMouseDown = false;
_selecting = false;
return true;
}
_caret = HitCharX(e.Data1);
_selAnchor = Selectable ? _caret : null;
_selecting = Selectable;
@ -403,7 +444,7 @@ public sealed class UiField : UiElement
{
switch (key)
{
case Silk.NET.Input.Key.A when Selectable: SelectAll(); return true;
case Silk.NET.Input.Key.A when Selectable: SelectAllText(); return true;
case Silk.NET.Input.Key.C when Selectable: CopySelection(); return true;
case Silk.NET.Input.Key.X when Selectable: CutSelection(); return true;
case Silk.NET.Input.Key.V: Paste(); return true;