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:
parent
8f222f7506
commit
41430420b3
8 changed files with 328 additions and 18 deletions
|
|
@ -747,22 +747,22 @@ public static class CharacterStatController
|
|||
var titleEl = ByPos(0f, 0f, FooterTitleId);
|
||||
if (titleEl is not null)
|
||||
{
|
||||
// The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so
|
||||
// without an override the text would center vertically in the 55px box, overlapping
|
||||
// line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the
|
||||
// top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
|
||||
// The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
|
||||
// BackgroundSprite cleared: its full-height sprite would cover line-1/line-2.
|
||||
titleEl.BackgroundSprite = 0;
|
||||
// The dat title element is H=55 (the full footer box). Centering the title text in it
|
||||
// lands dead-center (~y572), overlapping line-1/line-2 (y565/y582 — confirmed by a
|
||||
// position dump). Constrain it to a thin line at the footer top, and drop its anchors
|
||||
// so the per-frame stretch pass doesn't re-grow it back to 55.
|
||||
titleEl.Height = 18f;
|
||||
titleEl.Anchors = AnchorEdges.None;
|
||||
titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above)
|
||||
}
|
||||
// Title color: State A = body (parchment); State B = WHITE per retail (confirmed 2026-06-26 ref).
|
||||
if (titleEl is not null)
|
||||
{
|
||||
titleEl.DatFont = datFont;
|
||||
titleEl.Centered = false;
|
||||
titleEl.RightAligned = false;
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// RightAligned stays false (BuildText default for a Center element).
|
||||
titleEl.ClickThrough = true;
|
||||
titleEl.Padding = 0f;
|
||||
titleEl.LinesProvider = () =>
|
||||
{
|
||||
if (sel[0] < 0)
|
||||
|
|
|
|||
|
|
@ -239,13 +239,43 @@ public static class DatWidgetFactory
|
|||
/// 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>
|
||||
/// because <see cref="UiText.BackgroundColor"/> defaults to transparent.
|
||||
///
|
||||
/// <para>
|
||||
/// Justification from the dat (<see cref="ElementInfo.HJustify"/> /
|
||||
/// <see cref="ElementInfo.VJustify"/>) is applied here at build time so that controllers
|
||||
/// that subsequently call <see cref="UiText.Centered"/> / <see cref="UiText.RightAligned"/>
|
||||
/// on dat-origin elements can be simplified. Controllers that <em>explicitly</em> set those
|
||||
/// properties after <see cref="ImportedLayout.FindElement"/> still override the build-time
|
||||
/// defaults — the build-time value is just the starting point, not a lock.
|
||||
/// </para>
|
||||
/// </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 };
|
||||
|
||||
// Apply horizontal + vertical justification from the dat at build time.
|
||||
// Controllers that call FindElement and set Centered/RightAligned/VerticalJustify
|
||||
// afterward will override these — this is only the dat-driven default.
|
||||
bool centered = info.HJustify == HJustify.Center;
|
||||
bool rightAligned = info.HJustify == HJustify.Right;
|
||||
var vJustify = info.VJustify switch
|
||||
{
|
||||
VJustify.Top => VJustify.Top,
|
||||
VJustify.Bottom => VJustify.Bottom,
|
||||
_ => VJustify.Center,
|
||||
};
|
||||
|
||||
return new UiText
|
||||
{
|
||||
BackgroundSprite = bg,
|
||||
SpriteResolve = resolve,
|
||||
Centered = centered,
|
||||
RightAligned = rightAligned,
|
||||
VerticalJustify = vJustify,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@ using System.Collections.Generic;
|
|||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal text justification read from dat property 0x14 (UIElement HorizontalJustification).
|
||||
/// Values match the retail enum: 0=Left, 1=Center, 3/5=Right.
|
||||
/// </summary>
|
||||
public enum HJustify : byte { Left = 0, Center = 1, Right = 2 }
|
||||
|
||||
/// <summary>
|
||||
/// Vertical text justification read from dat property 0x15 (UIElement VerticalJustification).
|
||||
/// Values: 2=Top, 4=Bottom; absent/other = Center.
|
||||
/// </summary>
|
||||
public enum VJustify : byte { Top = 0, Center = 1, Bottom = 2 }
|
||||
|
||||
/// <summary>
|
||||
/// GL-free, dat-free snapshot of a resolved layout element.
|
||||
/// Populated by the LayoutDesc importer from <c>DatReaderWriter.ElementDesc</c>
|
||||
|
|
@ -51,6 +63,21 @@ public sealed class ElementInfo
|
|||
/// </summary>
|
||||
public uint FontDid;
|
||||
|
||||
/// <summary>
|
||||
/// Horizontal text justification from dat <c>Properties[0x14]</c>
|
||||
/// (<c>EnumBaseProperty</c>: 0=Left, 1=Center, 3/5=Right).
|
||||
/// Default is <see cref="HJustify.Center"/> to preserve existing behavior where
|
||||
/// controllers set <c>Centered=true</c> and no property was read.
|
||||
/// </summary>
|
||||
public HJustify HJustify = HJustify.Center;
|
||||
|
||||
/// <summary>
|
||||
/// Vertical text justification from dat <c>Properties[0x15]</c>
|
||||
/// (<c>EnumBaseProperty</c>: 2=Top, 4=Bottom; absent/other = Center).
|
||||
/// Default is <see cref="VJustify.Center"/> to preserve existing behavior.
|
||||
/// </summary>
|
||||
public VJustify VJustify = VJustify.Center;
|
||||
|
||||
/// <summary>
|
||||
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
|
||||
/// The <c>""</c> key represents the unnamed DirectState (<c>ElementDesc.StateDesc</c>).
|
||||
|
|
@ -160,6 +187,12 @@ public static class ElementReader
|
|||
ReadOrder = derived.ReadOrder,
|
||||
ZLevel = derived.ZLevel != 0 ? derived.ZLevel : base_.ZLevel,
|
||||
FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid,
|
||||
// HJustify/VJustify: derived wins when it carries an explicit non-Center value
|
||||
// (the dat property was present and read); otherwise inherit the base prototype's value.
|
||||
// Center is the default (= "not set by this element") so Center-derived never overrides
|
||||
// a non-Center base — matching the FontDid "non-zero wins" convention.
|
||||
HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify,
|
||||
VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify,
|
||||
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
|
||||
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
|
||||
// Children come from the derived element's own tree, not the base prototype's.
|
||||
|
|
|
|||
|
|
@ -362,6 +362,40 @@ public static class LayoutImporter
|
|||
{
|
||||
info.FontDid = did.Value;
|
||||
}
|
||||
|
||||
if (sd.Properties is not null)
|
||||
{
|
||||
// HorizontalJustification (0x14): EnumBaseProperty.
|
||||
// Retail values: 0=Left, 1=Center, 3=Right, 5=Right (treat 5 as Right per spec).
|
||||
// Only update if still at the default (Center); derived-wins handled in Merge.
|
||||
if (info.HJustify == HJustify.Center
|
||||
&& sd.Properties.TryGetValue(0x14u, out var hRaw)
|
||||
&& hRaw is EnumBaseProperty hEnum)
|
||||
{
|
||||
info.HJustify = hEnum.Value switch
|
||||
{
|
||||
0u => HJustify.Left,
|
||||
1u => HJustify.Center,
|
||||
3u => HJustify.Right,
|
||||
5u => HJustify.Right,
|
||||
_ => HJustify.Center, // unknown → center (safe default)
|
||||
};
|
||||
}
|
||||
|
||||
// VerticalJustification (0x15): EnumBaseProperty.
|
||||
// Retail values: 2=Top, 4=Bottom; absent/other = Center.
|
||||
if (info.VJustify == VJustify.Center
|
||||
&& sd.Properties.TryGetValue(0x15u, out var vRaw)
|
||||
&& vRaw is EnumBaseProperty vEnum)
|
||||
{
|
||||
info.VJustify = vEnum.Value switch
|
||||
{
|
||||
2u => VJustify.Top,
|
||||
4u => VJustify.Bottom,
|
||||
_ => VJustify.Center,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Prototype detection helpers ───────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue