feat(D.2b): UiText (Type 12) -- generic text + Type-12 flip; transcript factory-built (widget-generalization Task 5)
Rename UiChatView -> UiText (the retail UIElement_Text class, RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655). Factory changes (DatWidgetFactory.cs): - Remove the Type-12 skip (was: no-media -> null, with-media -> UiDatElement). - Add Type 12 -> BuildText() -> UiText in the switch. - BuildText extracts the element's Direct/Normal sprite as BackgroundSprite so any dat-media the element carried keeps rendering under the text. UiText changes (renamed from UiChatView.cs): - BackgroundColor default: (0,0,0,0.35) -> (0,0,0,0) (transparent). An unbound UiText draws nothing; the controller opts in to the translucent bg. - New BackgroundSprite + SpriteResolve: optional dat state-sprite background drawn UNDER DrawFill+text (faithful UIElement_Text media support). ChatWindowController.cs (Task 5 Step 8): - Transcript property: UiChatView -> UiText. - Bind() now uses layout.FindElement(TranscriptId) as UiText (factory-built) instead of manually constructing + AddChild-ing a new UiChatView. - Sets BackgroundColor = (0,0,0,0.35) on the found widget (retail translucent bg). - Removes the tInfo null-check from the early guard (transcript is factory-built; iInfo lookup kept for the input widget which is still manually constructed). - BuildLines: UiChatView.Line -> UiText.Line throughout. Vitals frozen: the Type-12 vitals number elements are meter children and are never recursed by BuildWidget (the `if (w is not UiMeter)` gate), so they are not built as widgets and keep rendering via UiMeter.Label. Vitals fixture vitals_2100006C.json unchanged; LayoutConformanceTests + VitalsBindingTests green. Tests: - UiChatViewTests.cs -> UiTextTests.cs (class: UiTextTests, all UiChatView.* -> UiText.*) - UiChatViewDatFontTests.cs -> UiTextDatFontTests.cs (same) - DatWidgetFactoryTests: delete Type12_StylePrototype_ReturnsNull + DatWidgetFactory_Type12WithMedia_Renders; add Type12_Text_MakesUiText + DatWidgetFactory_Type12_AlwaysMakesUiText. - LayoutImporterTests: BuildFromInfos_Type12Child_IsSkipped_Type3Present updated to assert IsType<UiText> (element is now in tree, transparent, not skipped). Divergence register: AP-37 amended -- removed the "standalone Type-0 text elements skipped / dat-text widget is Plan 2" clause (now shipped as UiText); kept the meter-collapse clause and the vitals-numbers-via-UiMeter.Label clause. AP-38/AP-39/AD-28 file references updated UiChatView.cs -> UiText.cs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67e5b8cff2
commit
cb082b59e4
10 changed files with 127 additions and 118 deletions
|
|
@ -65,7 +65,7 @@ public sealed class ChatWindowController
|
|||
public UiElement Root { get; private set; } = null!;
|
||||
|
||||
/// <summary>Live chat transcript widget. Null until <see cref="Bind"/> succeeds.</summary>
|
||||
public UiChatView Transcript { get; private set; } = null!;
|
||||
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!;
|
||||
|
|
@ -160,20 +160,20 @@ public sealed class ChatWindowController
|
|||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolve)
|
||||
{
|
||||
// The transcript + input nodes are Type-12 based and were skipped by the factory.
|
||||
// Find them in the raw ElementInfo tree to read their rects.
|
||||
var tInfo = FindInfo(rootInfo, TranscriptId);
|
||||
// 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.
|
||||
var iInfo = FindInfo(rootInfo, InputId);
|
||||
|
||||
// Their parent panels must exist as real widgets in the layout tree.
|
||||
var transcriptPanel = layout.FindElement(TranscriptPanelId);
|
||||
var inputBar = layout.FindElement(InputBarId);
|
||||
|
||||
if (tInfo is null || iInfo is null || transcriptPanel is null || inputBar is null)
|
||||
if (iInfo is null || transcriptPanel is null || inputBar is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[D.2b] ChatWindowController.Bind: missing required elements " +
|
||||
$"(tInfo={tInfo is not null}, iInfo={iInfo is not null}, " +
|
||||
$"(iInfo={iInfo is not null}, " +
|
||||
$"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " +
|
||||
$"chat window will not be interactive.");
|
||||
return null;
|
||||
|
|
@ -204,20 +204,14 @@ public sealed class ChatWindowController
|
|||
transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9)
|
||||
|
||||
// ── Transcript ───────────────────────────────────────────────────
|
||||
// Place the behavioral transcript widget inside the transcript panel at the
|
||||
// dat-rect of the (skipped) Type-12 transcript element.
|
||||
c.Transcript = new UiChatView
|
||||
{
|
||||
Left = tInfo.X,
|
||||
Top = tInfo.Y,
|
||||
Width = tInfo.Width,
|
||||
Height = tInfo.Height,
|
||||
Anchors = ElementReader.ToAnchors(tInfo.Left, tInfo.Top, tInfo.Right, tInfo.Bottom),
|
||||
DatFont = datFont,
|
||||
Font = debugFont,
|
||||
LinesProvider = () => BuildLines(vm, c.Transcript, datFont, debugFont),
|
||||
};
|
||||
transcriptPanel.AddChild(c.Transcript);
|
||||
// The factory now builds the Type-12 transcript element (0x10000011) as a UiText.
|
||||
// Find it in the widget tree and bind the live providers — no remove/add needed.
|
||||
c.Transcript = layout.FindElement(TranscriptId) as UiText
|
||||
?? throw new InvalidOperationException("chat transcript 0x10000011 not built as UiText");
|
||||
c.Transcript.DatFont = datFont;
|
||||
c.Transcript.Font = debugFont;
|
||||
c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript
|
||||
c.Transcript.LinesProvider = () => BuildLines(vm, c.Transcript, datFont, debugFont);
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────
|
||||
// Place the behavioral input widget inside the input bar.
|
||||
|
|
@ -373,14 +367,14 @@ public sealed class ChatWindowController
|
|||
|
||||
/// <summary>
|
||||
/// Convert the ChatVM's detailed lines to the transcript's
|
||||
/// <see cref="UiChatView.Line"/> record format, applying retail-faithful
|
||||
/// <see cref="UiText.Line"/> record format, applying retail-faithful
|
||||
/// per-<see cref="ChatKind"/> colors.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<UiChatView.Line> BuildLines(
|
||||
ChatVM vm, UiChatView view, UiDatFont? datFont, BitmapFont? debugFont)
|
||||
private static IReadOnlyList<UiText.Line> BuildLines(
|
||||
ChatVM vm, UiText view, UiDatFont? datFont, BitmapFont? debugFont)
|
||||
{
|
||||
var detailed = vm.RecentLinesDetailed();
|
||||
if (detailed.Count == 0) return Array.Empty<UiChatView.Line>();
|
||||
if (detailed.Count == 0) return Array.Empty<UiText.Line>();
|
||||
|
||||
// Word-wrap each message to the transcript's current pixel width (ports retail
|
||||
// GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
|
||||
|
|
@ -391,12 +385,12 @@ public sealed class ChatWindowController
|
|||
: debugFont is { } bf ? s => bf.MeasureWidth(s)
|
||||
: s => s.Length * 7f;
|
||||
|
||||
var result = new List<UiChatView.Line>(detailed.Count);
|
||||
var result = new List<UiText.Line>(detailed.Count);
|
||||
foreach (var d in detailed)
|
||||
{
|
||||
var color = RetailChatColor(d.Kind);
|
||||
foreach (var frag in WrapText(d.Text, maxW, measure))
|
||||
result.Add(new UiChatView.Line(frag, color));
|
||||
result.Add(new UiText.Line(frag, color));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <see cref="UiDatElement"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// Type 12 elements that carry NO own state media (pure style prototypes /
|
||||
/// BaseElement stores) return null from <see cref="Create"/> and are skipped.
|
||||
/// Type 12 elements that DO carry own sprites (e.g. a chat element whose Type-0
|
||||
/// derived form inherited Type 12 from its base prototype) are rendered normally.
|
||||
/// See docs/research/2026-06-15-layoutdesc-format.md Correction 8.
|
||||
/// Type 12 = UIElement_Text — a scrollable colored-line text view. Every Type-12
|
||||
/// element is now built as a <see cref="UiText"/>. Elements that carry their own
|
||||
/// dat sprite media keep it as the <see cref="UiText.BackgroundSprite"/>. Pure
|
||||
/// prototype elements (no state media, no controller binding) draw nothing because
|
||||
/// <see cref="UiText.BackgroundColor"/> defaults to transparent.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -45,23 +45,17 @@ public static class DatWidgetFactory
|
|||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
/// <param name="datFont">Retail UI font for the meter's "cur/max" number overlay.
|
||||
/// May be null pre-load — the meter falls back to the debug bitmap font.</param>
|
||||
/// <returns>The widget, or <c>null</c> for a pure Type-12 style prototype with no own sprites (caller skips it).</returns>
|
||||
/// <returns>The widget for this element. Never null — every type produces a widget.</returns>
|
||||
public static UiElement? Create(ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont)
|
||||
{
|
||||
// Type 12 = style prototype / BaseElement store referenced by BaseLayoutId.
|
||||
// PURE prototypes (no own state media) are property bags — never rendered; skip them.
|
||||
// A Type-12 element that carries its own state media (e.g. a chat Send button whose
|
||||
// Type-0 derived element inherited Type 12 from its base prototype) has sprites to
|
||||
// show and must render. See format doc §8 and the G1 task note.
|
||||
if (info.Type == 12 && info.StateMedia.Count == 0) return null;
|
||||
|
||||
UiElement e = info.Type switch
|
||||
{
|
||||
1 => new UiButton(info, resolve), // UIElement_Button (reg :125828)
|
||||
6 => new UiMenu(), // UIElement_Menu (reg :120163)
|
||||
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter
|
||||
11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137)
|
||||
12 => BuildText(info, resolve), // UIElement_Text (reg :115655)
|
||||
_ => new UiDatElement(info, resolve), // generic fallback for all other types
|
||||
};
|
||||
|
||||
|
|
@ -178,4 +172,20 @@ public static class DatWidgetFactory
|
|||
|
||||
return (left, tile, right);
|
||||
}
|
||||
|
||||
// ── Text ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Type-12 UIElement_Text: a scrollable colored-line text view. The element's
|
||||
/// own Direct/Normal media (if any) becomes the background sprite, drawn under the text —
|
||||
/// so a Type-12 element that previously rendered via UiDatElement keeps its sprite. Lines
|
||||
/// are bound later by the controller (LinesProvider). An unbound UiText draws nothing
|
||||
/// because <see cref="UiText.BackgroundColor"/> defaults to transparent.</summary>
|
||||
private static UiText BuildText(ElementInfo info, Func<uint, (uint, int, int)> resolve)
|
||||
{
|
||||
uint bg = info.StateMedia.TryGetValue(
|
||||
!string.IsNullOrEmpty(info.DefaultStateName) ? info.DefaultStateName
|
||||
: info.StateMedia.ContainsKey("Normal") ? "Normal" : "", out var m)
|
||||
? m.File : 0u;
|
||||
return new UiText { BackgroundSprite = bg, SpriteResolve = resolve };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
/// <summary>The last wired keyboard. Exposed so widgets that need clipboard
|
||||
/// access (<see cref="IKeyboard.ClipboardText"/>) or modifier-key state
|
||||
/// (<see cref="IKeyboard.IsKeyPressed"/>) — e.g. <see cref="UiChatView"/>'s
|
||||
/// (<see cref="IKeyboard.IsKeyPressed"/>) — e.g. <see cref="UiText"/>'s
|
||||
/// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins.</summary>
|
||||
public IKeyboard? Keyboard { get; private set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.UI;
|
|||
/// the scroll offset is an integer pixel value (<c>m_iScrollableY</c>) clamped to
|
||||
/// [0, ContentHeight - ViewHeight]; the thumb ratio is view/content; the position
|
||||
/// ratio is scroll/(content-view). Pure (no GL) so it is fully unit-tested and
|
||||
/// shared by the transcript (UiChatView) and the scrollbar (UiScrollbar).
|
||||
/// shared by the transcript (UiText) and the scrollbar (UiScrollbar).
|
||||
/// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0,
|
||||
/// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ using AcDream.App.Rendering;
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Scrollable chat transcript for the retail-look chat window. Renders the
|
||||
/// lines from <see cref="LinesProvider"/> bottom-pinned (newest at the bottom,
|
||||
/// Scrollable text view for retail UIElement_Text elements
|
||||
/// (<c>RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655</c>).
|
||||
/// Renders the lines from <see cref="LinesProvider"/> bottom-pinned (newest at the bottom,
|
||||
/// like retail) with mouse-wheel scrollback. Whole-line vertical clipping keeps
|
||||
/// text inside the window.
|
||||
///
|
||||
|
|
@ -19,7 +20,7 @@ namespace AcDream.App.UI;
|
|||
/// selected span to the clipboard. Ctrl+A selects everything.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiChatView : UiElement
|
||||
public sealed class UiText : UiElement
|
||||
{
|
||||
/// <summary>One display line: pre-formatted text + its colour.</summary>
|
||||
public readonly record struct Line(string Text, Vector4 Color);
|
||||
|
|
@ -43,8 +44,18 @@ public sealed class UiChatView : UiElement
|
|||
/// the host from <see cref="UiHost.Keyboard"/>.</summary>
|
||||
public Silk.NET.Input.IKeyboard? Keyboard { get; set; }
|
||||
|
||||
/// <summary>Backing fill behind the text (retail chat is a dark translucent box).</summary>
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.35f);
|
||||
/// <summary>Backing fill behind the text. Defaults to transparent so an unbound
|
||||
/// UiText (no controller) draws nothing. Set to the retail translucent value by
|
||||
/// the controller (e.g. <c>ChatWindowController</c>).</summary>
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0f);
|
||||
|
||||
/// <summary>Optional dat state-sprite background (the element's own media), drawn
|
||||
/// UNDER the text. Set by DatWidgetFactory.BuildText from the ElementInfo. 0 = none.</summary>
|
||||
public uint BackgroundSprite { get; set; }
|
||||
|
||||
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
|
||||
/// Required when <see cref="BackgroundSprite"/> is non-zero.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
/// <summary>Highlight colour painted behind a selected character span.</summary>
|
||||
public Vector4 SelectionColor { get; set; } = new(0.25f, 0.45f, 0.85f, 0.5f);
|
||||
|
|
@ -73,7 +84,7 @@ public sealed class UiChatView : UiElement
|
|||
private Pos? _selCaret; // where the drag currently is
|
||||
private bool _selecting;
|
||||
|
||||
public UiChatView()
|
||||
public UiText()
|
||||
{
|
||||
AcceptsFocus = true;
|
||||
IsEditControl = true; // absorb keys (Ctrl+C) while focused
|
||||
|
|
@ -93,6 +104,14 @@ public sealed class UiChatView : UiElement
|
|||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// Optional dat state-sprite background drawn UNDER everything else.
|
||||
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
|
||||
{
|
||||
var (tex, tw, th) = sr(BackgroundSprite);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
|
||||
// Background must draw UNDER the transcript text. DrawStringDat emits into the
|
||||
// sprite bucket which flushes BEFORE rects, so a DrawRect background would wash
|
||||
// over the text. DrawFill routes the background through the sprite bucket too,
|
||||
Loading…
Add table
Add a link
Reference in a new issue