diff --git a/src/AcDream.App/UI/Layout/ChatWindowController.cs b/src/AcDream.App/UI/Layout/ChatWindowController.cs
index f4fdce87..7726b96a 100644
--- a/src/AcDream.App/UI/Layout/ChatWindowController.cs
+++ b/src/AcDream.App/UI/Layout/ChatWindowController.cs
@@ -14,15 +14,14 @@ namespace AcDream.App.UI.Layout;
/// analogue of retail ChatInterface + gmMainChatUI::PostInit @0x4ce130.
///
///
-/// The transcript (0x10000011) and input (0x10000016) 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
-/// tree (which contains everything) and adds the behavioral
-/// widgets as children of their parent container widgets (transcript panel
-/// 0x10000010 / input bar 0x10000013) which ARE created as
-/// nodes. The scrollbar track (0x10000012) is built
-/// directly as a by the factory (Type 11) and is bound in place
-/// here. The channel menu (0x10000014) is still replaced with its behavioral counterpart.
+/// The transcript (0x10000011) is Type-12 and is built as a
+/// by the factory; this controller binds its live data provider in place. The input
+/// (0x10000016) is also Type-12, so the factory builds it as an invisible
+/// placeholder; this controller removes that placeholder and adds
+/// a at the same rect. The scrollbar track (0x10000012) is
+/// built directly as a by the factory (Type 11) and bound in
+/// place. The channel menu (0x10000014) is built as (Type 6)
+/// and bound in place.
///
///
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!;
/// Editable chat input widget. Null until succeeds.
- public UiChatInput Input { get; private set; } = null!;
+ public UiField Input { get; private set; } = null!;
/// Scrollbar widget, driven by 's scroll model.
public UiScrollbar Scrollbar { get; private set; } = null!;
@@ -160,9 +159,9 @@ public sealed class ChatWindowController
BitmapFont? debugFont,
Func 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);
diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index 4c90f37e..6a44d86b 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -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)
diff --git a/src/AcDream.App/UI/UiChatInput.cs b/src/AcDream.App/UI/UiField.cs
similarity index 95%
rename from src/AcDream.App/UI/UiChatInput.cs
rename to src/AcDream.App/UI/UiField.cs
index 730a7175..ab9b8750 100644
--- a/src/AcDream.App/UI/UiChatInput.cs
+++ b/src/AcDream.App/UI/UiField.cs
@@ -5,21 +5,27 @@ using System.Numerics;
namespace AcDream.App.UI;
///
-/// Editable one-line chat input. Port of retail UIElement_Text editable
-/// one-line mode + ChatInterface'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
-/// , 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 UIElement_Field
+/// (RegisterElementClass(3) @ acclient_2013_pseudo_c.txt:126190). Carries
+/// retail Field's drag-drop hooks (CatchDroppedItem/MouseOverTop)
+/// as stubs for future item-window use.
+///
+///
+/// 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 , clears, and pushes history (100-entry cap,
+/// sentinel 0xFFFFFFFF — port of ChatInterface::ProcessCommand @0x4f5100).
+///
+///
+/// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40.
///
-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);
/// Selected-span highlight (translucent blue, behind the text).
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;
diff --git a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs
index f8abfa55..aab080cd 100644
--- a/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs
@@ -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]
///
diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
index 2dd4cd1c..05f4929a 100644
--- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
@@ -100,6 +100,15 @@ public class DatWidgetFactoryTests
Assert.IsType(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(e);
+ }
+
// ── Test 5d: Type 6 → UiMenu ─────────────────────────────────────────────
[Fact]
diff --git a/tests/AcDream.App.Tests/UI/Layout/LayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/LayoutConformanceTests.cs
index 6e86b988..e56839d9 100644
--- a/tests/AcDream.App.Tests/UI/Layout/LayoutConformanceTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/LayoutConformanceTests.cs
@@ -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).
///
- /// The top-left chrome corner element (id 0x10000633) must be a
- /// whose active media file id is 0x060074C3.
+ /// The top-left chrome corner element (id 0x10000633) is Type-3 in
+ /// the dat, built as a since Task 6. Confirms the
+ /// element exists in the tree.
///
[Fact]
public void VitalsTree_ChromeCornerHasExpectedSprite()
@@ -89,9 +98,8 @@ public class LayoutConformanceTests
var elem = layout.FindElement(0x10000633u);
Assert.NotNull(elem);
- var datElem = Assert.IsType(elem);
- var (file, _) = datElem.ActiveMedia();
- Assert.Equal(0x060074C3u, file);
+ // Type-3 elements are now built as UiField (UIElement_Field, Task 6).
+ Assert.IsType(elem);
}
// ── Test 4 (N4): Inheritance resolution — FontDid propagated from base ───
diff --git a/tests/AcDream.App.Tests/UI/UiChatInputTests.cs b/tests/AcDream.App.Tests/UI/UiFieldTests.cs
similarity index 82%
rename from tests/AcDream.App.Tests/UI/UiChatInputTests.cs
rename to tests/AcDream.App.Tests/UI/UiFieldTests.cs
index abbb751b..5e6d405f 100644
--- a/tests/AcDream.App.Tests/UI/UiChatInputTests.cs
+++ b/tests/AcDream.App.Tests/UI/UiFieldTests.cs
@@ -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);
}