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)
|
||||
|
|
|
|||
|
|
@ -228,4 +228,75 @@ public class DatWidgetFactoryTests
|
|||
Assert.NotEqual(OverlayFile, m.FrontRight);
|
||||
Assert.NotEqual(OverlayFile, m.FrontTile);
|
||||
}
|
||||
|
||||
// ── Justification build-time application (new for importer Fix A) ────────
|
||||
|
||||
/// <summary>
|
||||
/// A Type-12 text element with HJustify=Center (the default) must produce a
|
||||
/// UiText with Centered=true and RightAligned=false at build time.
|
||||
/// This proves BuildText applies the dat's HJustify at construction without a
|
||||
/// controller binding step.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildText_HJustifyCenter_SetsCentered()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center };
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
Assert.True(t.Centered);
|
||||
Assert.False(t.RightAligned);
|
||||
Assert.Equal(VJustify.Center, t.VerticalJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Type-12 text element with HJustify=Right must produce a UiText with
|
||||
/// Centered=false and RightAligned=true at build time.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildText_HJustifyRight_SetsRightAligned()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Right };
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
Assert.False(t.Centered);
|
||||
Assert.True(t.RightAligned);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Type-12 text element with HJustify=Left must produce a UiText with
|
||||
/// Centered=false and RightAligned=false at build time.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildText_HJustifyLeft_SetsNeitherCenteredNorRight()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Left };
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
Assert.False(t.Centered);
|
||||
Assert.False(t.RightAligned);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Type-12 text element with VJustify=Top must produce a UiText with
|
||||
/// VerticalJustify=Top at build time.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildText_VJustifyTop_SetsVerticalJustifyTop()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 55, VJustify = VJustify.Top };
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
Assert.Equal(VJustify.Top, t.VerticalJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A controller override: build-time sets Centered=true (HJustify=Center), then
|
||||
/// a controller sets Centered=false afterward. The controller's override wins —
|
||||
/// this is the backward-compatibility guarantee.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildText_ControllerOverrideWinsAfterBuild()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center };
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
Assert.True(t.Centered); // build-time value
|
||||
t.Centered = false; // controller override
|
||||
Assert.False(t.Centered); // override wins
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,4 +161,57 @@ public class ElementReaderTests
|
|||
Assert.Single(merged.Children);
|
||||
Assert.Equal(0x2u, merged.Children[0].Id);
|
||||
}
|
||||
|
||||
// ── HJustify / VJustify — Merge propagation ─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// When the derived element has a non-Center HJustify, the derived value wins
|
||||
/// (same "non-default wins" rule as FontDid).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_DerivedHJustifyRight_OverridesBaseCenter()
|
||||
{
|
||||
var base_ = new ElementInfo { HJustify = HJustify.Center };
|
||||
var derived = new ElementInfo { HJustify = HJustify.Right };
|
||||
var merged = ElementReader.Merge(base_, derived);
|
||||
Assert.Equal(HJustify.Right, merged.HJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the derived element has the default HJustify (Center), the base value
|
||||
/// is inherited — Center from the derived does NOT override a Left base.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_DerivedHJustifyCenter_InheritsBaseLeft()
|
||||
{
|
||||
var base_ = new ElementInfo { HJustify = HJustify.Left };
|
||||
var derived = new ElementInfo { HJustify = HJustify.Center }; // default — no explicit dat property
|
||||
var merged = ElementReader.Merge(base_, derived);
|
||||
Assert.Equal(HJustify.Left, merged.HJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VJustify=Top from the base propagates when the derived element has no explicit
|
||||
/// (Center) vertical justification.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_BaseVJustifyTop_InheritedWhenDerivedIsCenter()
|
||||
{
|
||||
var base_ = new ElementInfo { VJustify = VJustify.Top };
|
||||
var derived = new ElementInfo { VJustify = VJustify.Center }; // default
|
||||
var merged = ElementReader.Merge(base_, derived);
|
||||
Assert.Equal(VJustify.Top, merged.VJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VJustify=Bottom from the derived element wins over a Center base.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Merge_DerivedVJustifyBottom_OverridesBaseCenter()
|
||||
{
|
||||
var base_ = new ElementInfo { VJustify = VJustify.Center };
|
||||
var derived = new ElementInfo { VJustify = VJustify.Bottom };
|
||||
var merged = ElementReader.Merge(base_, derived);
|
||||
Assert.Equal(VJustify.Bottom, merged.VJustify);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
|
|
@ -140,4 +141,57 @@ public class UiTextTests
|
|||
Assert.Equal(new UiText.Pos(1, 2), s2);
|
||||
Assert.Equal(new UiText.Pos(1, 8), e2);
|
||||
}
|
||||
|
||||
// ── VOffset: vertical positioning for single-line mode ───────────────────
|
||||
|
||||
/// <summary>
|
||||
/// VJustify.Center: vertical offset = (height - lineHeight) / 2.
|
||||
/// This is the original formula used by both the Centered and RightAligned paths
|
||||
/// before VerticalJustify was introduced. Must remain unchanged (backward compat).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VOffset_Center_ReturnsHalfHeightMinusLineHeight()
|
||||
{
|
||||
float y = UiText.VOffset(height: 55f, lineHeight: 12f, padding: 0f, VJustify.Center);
|
||||
Assert.Equal((55f - 12f) * 0.5f, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VJustify.Top: vertical offset = padding (top of the content area).
|
||||
/// Used for footer title elements whose dat box is the full footer height (55 px)
|
||||
/// but the text should render near the top.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VOffset_Top_ReturnsPadding()
|
||||
{
|
||||
float y = UiText.VOffset(height: 55f, lineHeight: 12f, padding: 0f, VJustify.Top);
|
||||
Assert.Equal(0f, y);
|
||||
|
||||
float yWithPadding = UiText.VOffset(height: 55f, lineHeight: 12f, padding: 4f, VJustify.Top);
|
||||
Assert.Equal(4f, yWithPadding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VJustify.Bottom: vertical offset = height - lineHeight - padding.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VOffset_Bottom_ReturnsHeightMinusLineHeightMinusPadding()
|
||||
{
|
||||
float y = UiText.VOffset(height: 55f, lineHeight: 12f, padding: 2f, VJustify.Bottom);
|
||||
Assert.Equal(55f - 12f - 2f, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Center with non-zero padding: padding does NOT affect the center formula —
|
||||
/// VJustify.Center always uses (height - lineHeight) / 2 regardless of padding,
|
||||
/// matching the original behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VOffset_Center_IgnoresPadding()
|
||||
{
|
||||
float yNoPad = UiText.VOffset(height: 40f, lineHeight: 12f, padding: 0f, VJustify.Center);
|
||||
float yWithPad = UiText.VOffset(height: 40f, lineHeight: 12f, padding: 4f, VJustify.Center);
|
||||
Assert.Equal(yNoPad, yWithPad); // Center is padding-independent
|
||||
Assert.Equal((40f - 12f) * 0.5f, yNoPad);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue