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:
parent
dc1649c493
commit
a20e5c68c7
19 changed files with 595 additions and 51 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue