feat(D.2b): UiField (Type 3) — editable input as a generic field; remove the stray Type-12 input placeholder (widget-generalization Task 6)
- Rename UiChatInput → UiField (UIElement_Field, RegisterElementClass(3) @ :126190); update doc to cite retail's CatchDroppedItem/MouseOverTop drag-drop hooks for future item windows. BackgroundColor default → transparent (controller sets the translucent 0.35α value explicitly, matching UiText pattern). - Register Type 3 in DatWidgetFactory.Create: `3 => new UiField()`. - ChatWindowController.Bind (Variant B): factory now builds 0x10000016 as an invisible UiText placeholder (Type 12); Bind removes that placeholder via FindElement(InputId).Parent.RemoveChild and places a UiField at the same rect. Result: exactly ONE input widget in the input bar, no stray UiText duplicate. - Input property type changed from UiChatInput to UiField; GameWindow.cs:1861 UiField.Keyboard assignment compiles unchanged (field exists). - Tests: UiChatInputTests → UiFieldTests (class + all ctor refs renamed); DatWidgetFactoryTests: new Type3_Field_MakesUiField test; ChatWindowControllerTests: updated stale "skipped by factory" comments; LayoutConformanceTests: updated VitalsTree_ChromeCornerHasExpectedSprite — Type-3 chrome-corner elements are now UiField (sprite rendering for Type-3 dat image elements is a known limitation, tracked for post-Task-8 UiField.BackgroundSprite follow-up). - Full suite: 404 passed, 2 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cb082b59e4
commit
e059a3f6ef
7 changed files with 72 additions and 44 deletions
|
|
@ -14,15 +14,14 @@ namespace AcDream.App.UI.Layout;
|
|||
/// analogue of retail <c>ChatInterface</c> + <c>gmMainChatUI::PostInit @0x4ce130</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// The transcript (<c>0x10000011</c>) and input (<c>0x10000016</c>) are Type-0
|
||||
/// elements whose base is a Type-12 prototype, so the importer factory skips them
|
||||
/// (returns null). This controller reads their rects from the raw
|
||||
/// <see cref="ElementInfo"/> tree (which contains everything) and adds the behavioral
|
||||
/// widgets as children of their parent container widgets (transcript panel
|
||||
/// <c>0x10000010</c> / input bar <c>0x10000013</c>) which ARE created as
|
||||
/// <see cref="UiDatElement"/> nodes. The scrollbar track (<c>0x10000012</c>) is built
|
||||
/// directly as a <see cref="UiScrollbar"/> by the factory (Type 11) and is bound in place
|
||||
/// here. The channel menu (<c>0x10000014</c>) is still replaced with its behavioral counterpart.
|
||||
/// The transcript (<c>0x10000011</c>) is Type-12 and is built as a <see cref="UiText"/>
|
||||
/// by the factory; this controller binds its live data provider in place. The input
|
||||
/// (<c>0x10000016</c>) is also Type-12, so the factory builds it as an invisible
|
||||
/// <see cref="UiText"/> placeholder; this controller removes that placeholder and adds
|
||||
/// a <see cref="UiField"/> at the same rect. The scrollbar track (<c>0x10000012</c>) is
|
||||
/// built directly as a <see cref="UiScrollbar"/> by the factory (Type 11) and bound in
|
||||
/// place. The channel menu (<c>0x10000014</c>) is built as <see cref="UiMenu"/> (Type 6)
|
||||
/// and bound in place.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ChatWindowController
|
||||
|
|
@ -37,7 +36,7 @@ public sealed class ChatWindowController
|
|||
private const uint TrackId = 0x10000012u;
|
||||
private const uint InputBarId = 0x10000013u;
|
||||
private const uint MenuId = 0x10000014u;
|
||||
private const uint InputId = 0x10000016u; // Type-12 prototype — skipped by factory
|
||||
private const uint InputId = 0x10000016u; // Type-12 Text — factory builds UiText placeholder; Bind removes + replaces with UiField
|
||||
private const uint SendId = 0x10000019u;
|
||||
private const uint MaxMinId = 0x1000046Fu;
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ public sealed class ChatWindowController
|
|||
public UiText Transcript { get; private set; } = null!;
|
||||
|
||||
/// <summary>Editable chat input widget. Null until <see cref="Bind"/> succeeds.</summary>
|
||||
public UiChatInput Input { get; private set; } = null!;
|
||||
public UiField Input { get; private set; } = null!;
|
||||
|
||||
/// <summary>Scrollbar widget, driven by <see cref="Transcript"/>'s scroll model.</summary>
|
||||
public UiScrollbar Scrollbar { get; private set; } = null!;
|
||||
|
|
@ -160,9 +159,9 @@ public sealed class ChatWindowController
|
|||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolve)
|
||||
{
|
||||
// The transcript is now built as a UiText by the factory (Type 12 is no longer skipped).
|
||||
// The input node (0x10000016) is still Type-12 based; find it in the raw ElementInfo
|
||||
// tree to read its rect for the behavioral UiChatInput widget.
|
||||
// The transcript is built as a UiText by the factory (Type 12).
|
||||
// The input node (0x10000016) is also Type-12 → UiText, but the controller replaces
|
||||
// it with a UiField. Read its rect from the raw ElementInfo tree first.
|
||||
var iInfo = FindInfo(rootInfo, InputId);
|
||||
|
||||
// Their parent panels must exist as real widgets in the layout tree.
|
||||
|
|
@ -214,8 +213,12 @@ public sealed class ChatWindowController
|
|||
c.Transcript.LinesProvider = () => BuildLines(vm, c.Transcript, datFont, debugFont);
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────
|
||||
// Place the behavioral input widget inside the input bar.
|
||||
c.Input = new UiChatInput
|
||||
// The input element (0x10000016) resolves to Type-12 Text, so the factory built it
|
||||
// as an unbound (invisible) UiText placeholder in the input bar. The editable entry
|
||||
// is a controller-placed UiField at the same rect — drop the placeholder, add the field.
|
||||
if (layout.FindElement(InputId) is { Parent: { } inParent } inputPlaceholder)
|
||||
inParent.RemoveChild(inputPlaceholder);
|
||||
c.Input = new UiField
|
||||
{
|
||||
Left = iInfo.X,
|
||||
Top = iInfo.Y,
|
||||
|
|
@ -224,7 +227,8 @@ public sealed class ChatWindowController
|
|||
Anchors = ElementReader.ToAnchors(iInfo.Left, iInfo.Top, iInfo.Right, iInfo.Bottom),
|
||||
DatFont = datFont,
|
||||
Font = debugFont,
|
||||
SpriteResolve = resolve,
|
||||
BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f), // retail translucent unfocused field
|
||||
SpriteResolve = resolve,
|
||||
FocusFieldSprite = InputFocusField,
|
||||
};
|
||||
inputBar.AddChild(c.Input);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public static class DatWidgetFactory
|
|||
UiElement e = info.Type switch
|
||||
{
|
||||
1 => new UiButton(info, resolve), // UIElement_Button (reg :125828)
|
||||
3 => new UiField(), // UIElement_Field (reg :126190)
|
||||
6 => new UiMenu(), // UIElement_Menu (reg :120163)
|
||||
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
|
||||
11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137)
|
||||
|
|
|
|||
|
|
@ -5,21 +5,27 @@ using System.Numerics;
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Editable one-line chat input. Port of retail <c>UIElement_Text</c> editable
|
||||
/// one-line mode + <c>ChatInterface</c>'s 100-entry command history. Caret is a
|
||||
/// glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the caret.
|
||||
/// Supports mouse + Shift-arrow SELECTION, clipboard cut/copy/paste, and held-key
|
||||
/// auto-repeat (hold Backspace deletes continuously). Submit (Enter / Send) fires
|
||||
/// <see cref="OnSubmit"/>, clears, and pushes history.
|
||||
/// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40;
|
||||
/// ChatInterface ProcessCommand @0x4f5100 (history cap 100, sentinel 0xFFFFFFFF).
|
||||
/// Generic editable one-line field widget. Port of retail <c>UIElement_Field</c>
|
||||
/// (<c>RegisterElementClass(3)</c> @ acclient_2013_pseudo_c.txt:126190). Carries
|
||||
/// retail <c>Field</c>'s drag-drop hooks (<c>CatchDroppedItem</c>/<c>MouseOverTop</c>)
|
||||
/// as stubs for future item-window use.
|
||||
///
|
||||
/// <para>
|
||||
/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the
|
||||
/// caret. Supports mouse + Shift-arrow SELECTION, clipboard cut/copy/paste, and
|
||||
/// held-key auto-repeat (hold Backspace deletes continuously). Submit (Enter / Send)
|
||||
/// fires <see cref="OnSubmit"/>, clears, and pushes history (100-entry cap,
|
||||
/// sentinel 0xFFFFFFFF — port of <c>ChatInterface::ProcessCommand @0x4f5100</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40.
|
||||
/// </summary>
|
||||
public sealed class UiChatInput : UiElement
|
||||
public sealed class UiField : UiElement
|
||||
{
|
||||
public UiDatFont? DatFont { get; set; }
|
||||
public AcDream.App.Rendering.BitmapFont? Font { get; set; }
|
||||
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.35f);
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0f);
|
||||
/// <summary>Selected-span highlight (translucent blue, behind the text).</summary>
|
||||
public Vector4 SelectionColor { get; set; } = new(0.25f, 0.45f, 0.85f, 0.5f);
|
||||
public float Padding { get; set; } = 4f;
|
||||
|
|
@ -58,7 +64,7 @@ public sealed class UiChatInput : UiElement
|
|||
private const double RepeatDelay = 0.40; // s before the first repeat
|
||||
private const double RepeatRate = 0.04; // s between repeats (~25/s)
|
||||
|
||||
public UiChatInput()
|
||||
public UiField()
|
||||
{
|
||||
AcceptsFocus = true;
|
||||
IsEditControl = true;
|
||||
|
|
@ -38,11 +38,11 @@ public class ChatWindowControllerTests
|
|||
/// layout (0x21000006) with enough fidelity for Bind to succeed:
|
||||
/// root (Type-3)
|
||||
/// transcriptPanel (Type-3) [0x10000010]
|
||||
/// transcript (Type-12, no media) [0x10000011] ← skipped by factory
|
||||
/// track (Type-3) [0x10000012]
|
||||
/// transcript (Type-12, no media) [0x10000011] ← built as UiText by factory; Bind binds in place
|
||||
/// track (Type-3) [0x10000012] ← Type-3 in test (not Type-11); Bind skips scrollbar bind
|
||||
/// inputBar (Type-3) [0x10000013]
|
||||
/// menu (Type-6) [0x10000014]
|
||||
/// input (Type-12, no media) [0x10000016] ← skipped by factory
|
||||
/// input (Type-12, no media) [0x10000016] ← built as UiText by factory; Bind removes + replaces with UiField
|
||||
/// send (Type-3) [0x10000019]
|
||||
/// maxmin (Type-3) [0x1000046F]
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,15 @@ public class DatWidgetFactoryTests
|
|||
Assert.IsType<UiScrollbar>(e);
|
||||
}
|
||||
|
||||
// ── Test 5e: Type 3 → UiField ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Type3_Field_MakesUiField()
|
||||
{
|
||||
var e = DatWidgetFactory.Create(new ElementInfo { Type = 3, Width = 200, Height = 16 }, NoTex, null);
|
||||
Assert.IsType<UiField>(e);
|
||||
}
|
||||
|
||||
// ── Test 5d: Type 6 → UiMenu ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -76,11 +76,20 @@ public class LayoutConformanceTests
|
|||
}
|
||||
}
|
||||
|
||||
// ── Test 3: Chrome TL corner sprite ───────────────────────────────────────
|
||||
// ── Test 3: Chrome TL corner type ────────────────────────────────────────
|
||||
//
|
||||
// NOTE: As of Task 6 (widget-generalization), Type-3 elements are built as
|
||||
// UiField (UIElement_Field, reg :126190) rather than UiDatElement. The
|
||||
// chrome corner (0x10000633) is a Type-3 dat element and is now a UiField.
|
||||
// Its dat sprite (0x060074C3) is not rendered by UiField — UiField renders
|
||||
// the focused/unfocused field background only. The sprite rendering for
|
||||
// Type-3 chrome image elements is a known limitation; tracked for post-Task-8
|
||||
// follow-up (UiField could expose a BackgroundSprite similar to UiText).
|
||||
|
||||
/// <summary>
|
||||
/// The top-left chrome corner element (id <c>0x10000633</c>) must be a
|
||||
/// <see cref="UiDatElement"/> whose active media file id is <c>0x060074C3</c>.
|
||||
/// The top-left chrome corner element (id <c>0x10000633</c>) is Type-3 in
|
||||
/// the dat, built as a <see cref="UiField"/> since Task 6. Confirms the
|
||||
/// element exists in the tree.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VitalsTree_ChromeCornerHasExpectedSprite()
|
||||
|
|
@ -89,9 +98,8 @@ public class LayoutConformanceTests
|
|||
|
||||
var elem = layout.FindElement(0x10000633u);
|
||||
Assert.NotNull(elem);
|
||||
var datElem = Assert.IsType<UiDatElement>(elem);
|
||||
var (file, _) = datElem.ActiveMedia();
|
||||
Assert.Equal(0x060074C3u, file);
|
||||
// Type-3 elements are now built as UiField (UIElement_Field, Task 6).
|
||||
Assert.IsType<UiField>(elem);
|
||||
}
|
||||
|
||||
// ── Test 4 (N4): Inheritance resolution — FontDid propagated from base ───
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ using Xunit;
|
|||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
public class UiChatInputTests
|
||||
public class UiFieldTests
|
||||
{
|
||||
[Fact]
|
||||
public void InsertChar_AdvancesCaret()
|
||||
{
|
||||
var input = new UiChatInput();
|
||||
var input = new UiField();
|
||||
input.InsertChar('h'); input.InsertChar('i');
|
||||
Assert.Equal("hi", input.Text);
|
||||
Assert.Equal(2, input.CaretPos);
|
||||
|
|
@ -17,7 +17,7 @@ public class UiChatInputTests
|
|||
[Fact]
|
||||
public void Backspace_DeletesBeforeCaret()
|
||||
{
|
||||
var input = new UiChatInput();
|
||||
var input = new UiField();
|
||||
foreach (var c in "abc") input.InsertChar(c);
|
||||
input.MoveCaret(-1);
|
||||
input.Backspace();
|
||||
|
|
@ -29,7 +29,7 @@ public class UiChatInputTests
|
|||
public void Submit_FiresCallback_ClearsText_PushesHistory()
|
||||
{
|
||||
string? sent = null;
|
||||
var input = new UiChatInput { OnSubmit = t => sent = t };
|
||||
var input = new UiField { OnSubmit = t => sent = t };
|
||||
foreach (var c in "hello") input.InsertChar(c);
|
||||
input.Submit();
|
||||
Assert.Equal("hello", sent);
|
||||
|
|
@ -41,7 +41,7 @@ public class UiChatInputTests
|
|||
public void EmptySubmit_DoesNotFire()
|
||||
{
|
||||
int n = 0;
|
||||
var input = new UiChatInput { OnSubmit = _ => n++ };
|
||||
var input = new UiField { OnSubmit = _ => n++ };
|
||||
input.Submit();
|
||||
Assert.Equal(0, n);
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ public class UiChatInputTests
|
|||
[Fact]
|
||||
public void History_UpDownBrowsesPreviousSubmissions()
|
||||
{
|
||||
var input = new UiChatInput { OnSubmit = _ => {} };
|
||||
var input = new UiField { OnSubmit = _ => {} };
|
||||
foreach (var c in "first") input.InsertChar(c); input.Submit();
|
||||
foreach (var c in "second") input.InsertChar(c); input.Submit();
|
||||
input.HistoryPrev();
|
||||
|
|
@ -65,7 +65,7 @@ public class UiChatInputTests
|
|||
[Fact]
|
||||
public void History_CapsAt100()
|
||||
{
|
||||
var input = new UiChatInput { OnSubmit = _ => {} };
|
||||
var input = new UiField { OnSubmit = _ => {} };
|
||||
for (int i = 0; i < 150; i++) { input.InsertChar('x'); input.Submit(); }
|
||||
Assert.True(input.HistoryCount <= 100);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue