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>
57 lines
2.8 KiB
C#
57 lines
2.8 KiB
C#
using System;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Pixel-based vertical scroll model. Port of retail <c>UIElement_Scrollable</c>:
|
|
/// 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 (UiText) and the scrollbar (UiScrollbar).
|
|
/// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0,
|
|
/// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0.
|
|
/// </summary>
|
|
public sealed class UiScrollable
|
|
{
|
|
/// <summary>Total wrapped content height in px (m_iScrollableHeight).</summary>
|
|
public int ContentHeight { get; set; }
|
|
/// <summary>Visible viewport height in px.</summary>
|
|
public int ViewHeight { get; set; }
|
|
/// <summary>Pixels per text line (scroll quantum). InqScrollDelta line case.</summary>
|
|
public int LineHeight { get; set; } = 16;
|
|
|
|
private int _scrollY;
|
|
/// <summary>Current scroll offset in px from the top of the content.</summary>
|
|
public int ScrollY => _scrollY;
|
|
|
|
/// <summary>Max scroll = max(0, content - view).</summary>
|
|
public int MaxScroll => Math.Max(0, ContentHeight - ViewHeight);
|
|
|
|
/// <summary>True when content exceeds the view (a scrollbar is warranted).</summary>
|
|
public bool HasOverflow => ContentHeight > ViewHeight;
|
|
|
|
/// <summary>True when the offset is at (or past) the bottom — used for bottom-pin.</summary>
|
|
public bool AtEnd => _scrollY >= MaxScroll;
|
|
|
|
/// <summary>Set the offset, clamped to [0, MaxScroll] (SetScrollableXY clamp).</summary>
|
|
public void SetScrollY(int y) => _scrollY = Math.Clamp(y, 0, MaxScroll);
|
|
|
|
/// <summary>Pin to the bottom (newest content visible).</summary>
|
|
public void ScrollToEnd() => _scrollY = MaxScroll;
|
|
|
|
/// <summary>Thumb size ratio = view/content, clamped to 1 (UpdateScrollbarSize_).</summary>
|
|
public float ThumbRatio => ContentHeight <= 0 ? 1f : Math.Min(1f, (float)ViewHeight / ContentHeight);
|
|
|
|
/// <summary>Position ratio = scroll/(content-view) in [0,1] (UpdateScrollbarPosition_).</summary>
|
|
public float PositionRatio => MaxScroll <= 0 ? 0f : (float)_scrollY / MaxScroll;
|
|
|
|
/// <summary>Inverse of PositionRatio — used when the user drags the thumb.</summary>
|
|
public void SetPositionRatio(float ratio)
|
|
=> SetScrollY((int)MathF.Round(Math.Clamp(ratio, 0f, 1f) * MaxScroll));
|
|
|
|
/// <summary>Scroll by whole lines (sign: +down/newer, -up/older).</summary>
|
|
public void ScrollByLines(int lines) => SetScrollY(_scrollY + lines * LineHeight);
|
|
|
|
/// <summary>Scroll by a page = one view height (InqScrollDelta page case).</summary>
|
|
public void ScrollByPage(int pages) => SetScrollY(_scrollY + pages * ViewHeight);
|
|
}
|