feat(ui): importer carries dat justification (0x14/0x15) onto text widgets; drop character footer-title height hack

LayoutImporter.ReadState now reads Properties[0x14] (HorizontalJustification,
EnumBaseProperty: 0=Left, 1=Center, 3/5=Right) and Properties[0x15]
(VerticalJustification: 2=Top, 4=Bottom; else Center) into two new ElementInfo
fields HJustify/VJustify. Merge propagates them with the same non-default-wins
rule used for FontDid.

DatWidgetFactory.BuildText applies the resolved justify at build time:
HJustify=Center sets Centered=true, HJustify=Right sets RightAligned=true,
VJustify=Top/Bottom sets VerticalJustify. Controllers that FindElement and set
those properties afterward continue to override - backward-compat preserved.

UiText gains VerticalJustify (Top/Center/Bottom, default Center). The Centered
and RightAligned single-line paths call UiText.VOffset() for the Y coordinate,
so VJustify.Top renders text at y=Padding rather than the fixed (H-lh)/2 center.

CharacterStatController: footer title (0x1000024E, H=55 dat box) previously
used Height=18 + Anchors=None to prevent center-vertical overlap with line-1/2.
Diagnostic confirmed dat says HJustify=Center, VJustify=Center. The hack is
replaced with one minimal explicit override: VerticalJustify=Top on the
already-Centered element. Text now renders at top of the 55px box natively.
Centered=false and RightAligned=false hand-sets removed where dat supplies them.

Dat justify values (studio diagnostic, 2026-06-26):
  0x1000024E footer title: HJustify=Center, VJustify=Center
  0x10000235/0x10000243/0x10000245 value fields: HJustify=Right
  header name/heritage/pk/level: HJustify=Center

+13 tests: ElementReader Merge propagation; DatWidgetFactory BuildText
justify application + controller-override backward-compat; UiText VOffset.
696 passed, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-26 10:05:36 +02:00
parent 8f222f7506
commit 41430420b3
8 changed files with 328 additions and 18 deletions

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics;
using System.Text;
using AcDream.App.Rendering;
using AcDream.App.UI.Layout;
namespace AcDream.App.UI;
@ -78,6 +79,23 @@ public sealed class UiText : UiElement
/// precedence. Pair with <c>ClickThrough = true</c> for non-interactive labels.</summary>
public bool RightAligned { get; set; }
/// <summary>
/// Vertical position of the text within the element rect in single-line mode
/// (<see cref="Centered"/> or <see cref="RightAligned"/>).
/// <list type="bullet">
/// <item><description><b>Center</b> (default) — vertically centered, matching the original
/// behavior of the centered/right-aligned paths.</description></item>
/// <item><description><b>Top</b> — text is placed at <c>y = Padding</c> (top of the content
/// area), so the text sits at the top of the element rather than centering in it.
/// Used for footer title elements whose dat box is the full footer height (55 px) but
/// the text should render near the top.</description></item>
/// <item><description><b>Bottom</b> — text is placed at <c>y = Height - lineHeight - Padding</c>.</description></item>
/// </list>
/// Only meaningful when <see cref="Centered"/> or <see cref="RightAligned"/> is true.
/// Has no effect on the scrollable multi-line path.
/// </summary>
public VJustify VerticalJustify { get; set; } = VJustify.Center;
/// <summary>The scroll model — also read by the linked UiScrollbar.</summary>
public UiScrollable Scroll { get; } = new();
@ -138,8 +156,8 @@ public sealed class UiText : UiElement
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
// Static centered single-line mode (vitals cur/max numbers etc.): draw the first
// line centered H+V with the SAME formula UIElement_Meter used for its label, then
// skip the scroll/selection machinery entirely.
// 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)
{
var cLines = LinesProvider();
@ -148,20 +166,20 @@ public sealed class UiText : UiElement
if (DatFont is { } cdf)
{
float cx = (Width - cdf.MeasureWidth(line0.Text)) * 0.5f;
float cy = (Height - cdf.LineHeight) * 0.5f;
float cy = VOffset(Height, cdf.LineHeight, Padding, VerticalJustify);
ctx.DrawStringDat(cdf, line0.Text, cx, cy, line0.Color);
}
else if ((Font ?? ctx.DefaultFont) is { } cbf)
{
float cx = (Width - cbf.MeasureWidth(line0.Text)) * 0.5f;
float cy = (Height - cbf.LineHeight) * 0.5f;
float cy = VOffset(Height, cbf.LineHeight, Padding, VerticalJustify);
ctx.DrawString(line0.Text, cx, cy, line0.Color, cbf);
}
return;
}
// Static right-aligned single-line mode: draw the first line flush with the right
// edge, vertically centered, then skip the scroll/selection machinery.
// edge, vertical position per VerticalJustify, then skip the scroll/selection machinery.
if (RightAligned)
{
var rLines = LinesProvider();
@ -170,13 +188,13 @@ public sealed class UiText : UiElement
if (DatFont is { } rdf)
{
float rx = Width - rdf.MeasureWidth(line0.Text) - Padding;
float ry = (Height - rdf.LineHeight) * 0.5f;
float ry = VOffset(Height, rdf.LineHeight, Padding, VerticalJustify);
ctx.DrawStringDat(rdf, line0.Text, rx, ry, line0.Color);
}
else if ((Font ?? ctx.DefaultFont) is { } rbf)
{
float rx = Width - rbf.MeasureWidth(line0.Text) - Padding;
float ry = (Height - rbf.LineHeight) * 0.5f;
float ry = VOffset(Height, rbf.LineHeight, Padding, VerticalJustify);
ctx.DrawString(line0.Text, rx, ry, line0.Color, rbf);
}
return;
@ -366,6 +384,23 @@ public sealed class UiText : UiElement
// ── Pure, testable logic (no GL / no font texture) ───────────────────
/// <summary>
/// Compute the Y offset (local space) for a single line in the Centered/RightAligned
/// single-line path, given the element height, font line-height, padding, and
/// vertical justification.
/// </summary>
/// <param name="height">Element height in pixels.</param>
/// <param name="lineHeight">Font line height in pixels.</param>
/// <param name="padding">Content padding.</param>
/// <param name="vj">Vertical justification.</param>
public static float VOffset(float height, float lineHeight, float padding, VJustify vj)
=> vj switch
{
VJustify.Top => padding,
VJustify.Bottom => height - lineHeight - padding,
_ => (height - lineHeight) * 0.5f, // Center (default)
};
/// <summary>Order two caret positions so the first is <= the second (by line,
/// then column).</summary>
public static (Pos start, Pos end) Order(Pos a, Pos b)