feat(ui): port retained widget foundations

This commit is contained in:
Erik 2026-07-10 17:55:41 +02:00
parent 44f9ec13d9
commit d825572e31
44 changed files with 84813 additions and 292 deletions

View file

@ -15,10 +15,9 @@ namespace AcDream.App.UI;
/// text inside the window.
///
/// <para>
/// Supports Windows-like text selection: a left-click-drag inside the transcript
/// selects characters (the <see cref="UiElement.CapturesPointerDrag"/> opt-out
/// stops that interior drag from moving the host window), and Ctrl+C copies the
/// selected span to the clipboard. Ctrl+A selects everything.
/// When retail Selectable property `0x27` is enabled, left-click-drag selects
/// characters, Ctrl+C copies the selected span, and Ctrl+A selects everything.
/// Display-only text remains click-through and cannot steal focus or window drag.
/// </para>
/// </summary>
public sealed class UiText : UiElement
@ -80,6 +79,37 @@ public sealed class UiText : UiElement
/// <summary>Inner text inset from the view edges, px.</summary>
public float Padding { get; set; } = 4f;
/// <summary>Retail property 0x20. Independent of horizontal/vertical
/// justification; false permits the normal multi-line layout path.</summary>
public bool OneLine { get; set; }
private bool _selectable;
/// <summary>
/// Retail property 0x27. A display-only text element does not claim the mouse,
/// focus, or pointer drag. Selection and clipboard behavior are enabled only
/// when this capability is true.
/// </summary>
public bool Selectable
{
get => _selectable;
set
{
if (_selectable == value) return;
_selectable = value;
ClickThrough = !value;
AcceptsFocus = value;
IsEditControl = value;
CapturesPointerDrag = value;
if (!value)
{
_selecting = false;
_selAnchor = null;
_selCaret = null;
}
}
}
/// <summary>Static centered single-line mode (retail <c>UIElement_Text</c> center
/// justification): draws the FIRST line centered horizontally AND vertically in the
/// element rect, with NO scroll/selection machinery. Used for static labels such as
@ -135,9 +165,11 @@ public sealed class UiText : UiElement
public UiText()
{
AcceptsFocus = true;
IsEditControl = true; // absorb keys (Ctrl+C) while focused
CapturesPointerDrag = true; // interior drag selects, doesn't move the window
// UIElement_Text starts display-only (m_bitField = DIRTY|CURSOR_VISIBLE).
ClickThrough = true;
AcceptsFocus = false;
IsEditControl = false;
CapturesPointerDrag = false;
}
/// <summary>The text view draws its own lines + background; any dat sub-elements
@ -174,7 +206,7 @@ public sealed class UiText : UiElement
// Static centered single-line mode (vitals cur/max numbers etc.): draw the first
// line centered H+V (or H+Top/Bottom per VerticalJustify) with the SAME formula
// UIElement_Meter used for its label, then skip the scroll/selection machinery entirely.
if (Centered)
if (OneLine && Centered)
{
var cLines = LinesProvider();
if (cLines.Count == 0) return;
@ -196,7 +228,7 @@ public sealed class UiText : UiElement
// Static right-aligned single-line mode: draw the first line flush with the right
// edge, vertical position per VerticalJustify, then skip the scroll/selection machinery.
if (RightAligned)
if (OneLine && RightAligned)
{
var rLines = LinesProvider();
if (rLines.Count == 0) return;
@ -216,6 +248,26 @@ public sealed class UiText : UiElement
return;
}
// One-line is an independent text-layout property. Left justification uses
// the same vertical policy without turning alignment into a line-count flag.
if (OneLine)
{
var singleLines = LinesProvider();
if (singleLines.Count == 0) return;
var line0 = singleLines[0];
if (DatFont is { } datSingle)
{
float y = VOffset(Height, datSingle.LineHeight, Padding, VerticalJustify);
ctx.DrawStringDat(datSingle, line0.Text, Padding, y, line0.Color);
}
else if ((Font ?? ctx.DefaultFont) is { } bitmapSingle)
{
float y = VOffset(Height, bitmapSingle.LineHeight, Padding, VerticalJustify);
ctx.DrawString(line0.Text, Padding, y, line0.Color, bitmapSingle);
}
return;
}
// Prefer the retail dat font when set; fall back to BitmapFont.
var datFont = DatFont;
var bitmapFont = datFont is null ? (Font ?? ctx.DefaultFont) : null;
@ -301,6 +353,7 @@ public sealed class UiText : UiElement
{
case UiEventType.Scroll:
{
if (!Selectable) return false;
// Silk wheel +Y = scroll up = reveal older = toward the TOP = decrease ScrollY.
// ScrollByLines sign: +down/newer, -up/older.
// e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines.
@ -311,6 +364,7 @@ public sealed class UiText : UiElement
case UiEventType.MouseDown:
{
if (!Selectable) return false;
// Data1/Data2 = local-to-target coords (UiRoot.OnMouseDown).
var p = HitChar(e.Data1, e.Data2);
_selAnchor = p;
@ -321,6 +375,7 @@ public sealed class UiText : UiElement
case UiEventType.MouseMove:
{
if (!Selectable) return false;
if (_selecting)
{
// Data1/Data2 = local-to-target coords (DispatchMouseMove).
@ -332,12 +387,14 @@ public sealed class UiText : UiElement
case UiEventType.MouseUp:
{
if (!Selectable) return false;
_selecting = false;
return true;
}
case UiEventType.KeyDown:
{
if (!Selectable) return false;
var key = (Silk.NET.Input.Key)e.Data0;
bool ctrl = Keyboard is not null
&& (Keyboard.IsKeyPressed(Silk.NET.Input.Key.ControlLeft)