using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI; public class UiTextTests { [Fact] public void ClampScroll_PinsToZero_WhenContentFitsView() { // 5 lines of content in a taller view → nothing to scroll, pinned at 0. Assert.Equal(0f, UiText.ClampScroll(50f, contentHeight: 80f, viewHeight: 200f)); Assert.Equal(0f, UiText.ClampScroll(0f, contentHeight: 80f, viewHeight: 200f)); } [Fact] public void ClampScroll_CapsAtContentMinusView_WhenOverflowing() { // Content 500, view 200 → max scrollback is 300px (oldest line at top). Assert.Equal(300f, UiText.ClampScroll(1000f, contentHeight: 500f, viewHeight: 200f)); Assert.Equal(120f, UiText.ClampScroll(120f, contentHeight: 500f, viewHeight: 200f)); } [Fact] public void ClampScroll_NeverNegative() { Assert.Equal(0f, UiText.ClampScroll(-50f, contentHeight: 500f, viewHeight: 200f)); } // ── Char-index hit-testing (x → col) with a synthetic 10px monospace advance ── private static readonly Func Mono10 = static _ => 10f; [Fact] public void CharIndexAt_ZeroOrNegative_IsColumnZero() { Assert.Equal(0, UiText.CharIndexAt("hello", Mono10, 0f)); Assert.Equal(0, UiText.CharIndexAt("hello", Mono10, -5f)); } [Fact] public void CharIndexAt_SnapsToGlyphMidpoint() { // glyph[0] spans 0..10 (midpoint 5), glyph[1] 10..20 (midpoint 15), ... Assert.Equal(0, UiText.CharIndexAt("hello", Mono10, 4f)); // before mid of glyph 0 Assert.Equal(1, UiText.CharIndexAt("hello", Mono10, 6f)); // past mid of glyph 0 Assert.Equal(1, UiText.CharIndexAt("hello", Mono10, 14f)); // before mid of glyph 1 Assert.Equal(2, UiText.CharIndexAt("hello", Mono10, 16f)); // past mid of glyph 1 } [Fact] public void CharIndexAt_PastEnd_IsLength() { Assert.Equal(5, UiText.CharIndexAt("hello", Mono10, 1000f)); } [Fact] public void CharIndexAt_EmptyString_IsZero() { Assert.Equal(0, UiText.CharIndexAt("", Mono10, 50f)); } // ── SelectedText assembly ──────────────────────────────────────────── private static IReadOnlyList Lines(params string[] texts) { var list = new List(texts.Length); foreach (var t in texts) list.Add(new UiText.Line(t, new Vector4(1, 1, 1, 1))); return list; } [Fact] public void SelectedText_SingleLine_Substring() { var lines = Lines("hello world"); var s = UiText.SelectedText(lines, new UiText.Pos(0, 6), new UiText.Pos(0, 11)); Assert.Equal("world", s); } [Fact] public void SelectedText_SingleLine_ReversedAnchorCaret_IsNormalised() { var lines = Lines("hello world"); // caret BEFORE anchor — Order() must normalise. var s = UiText.SelectedText(lines, new UiText.Pos(0, 11), new UiText.Pos(0, 6)); Assert.Equal("world", s); } [Fact] public void SelectedText_SamePosition_IsEmpty() { var lines = Lines("hello"); Assert.Equal("", UiText.SelectedText(lines, new UiText.Pos(0, 3), new UiText.Pos(0, 3))); } [Fact] public void SelectedText_MultiLine_JoinsWithNewline() { var lines = Lines("first line", "second line", "third line"); // from col 6 of line 0 ("line") through col 5 of line 2 ("third") var s = UiText.SelectedText(lines, new UiText.Pos(0, 6), new UiText.Pos(2, 5)); Assert.Equal("line\nsecond line\nthird", s); } [Fact] public void SelectedText_MultiLine_TwoLines_NoMiddle() { var lines = Lines("alpha", "bravo"); var s = UiText.SelectedText(lines, new UiText.Pos(0, 2), new UiText.Pos(1, 3)); Assert.Equal("pha\nbra", s); } [Fact] public void SelectedText_MultiLine_ReversedAnchorCaret_IsNormalised() { var lines = Lines("alpha", "bravo"); // end before start → Order() swaps them. var s = UiText.SelectedText(lines, new UiText.Pos(1, 3), new UiText.Pos(0, 2)); Assert.Equal("pha\nbra", s); } [Fact] public void SelectedText_EmptyLineList_IsEmpty() { Assert.Equal("", UiText.SelectedText(Array.Empty(), new UiText.Pos(0, 0), new UiText.Pos(0, 0))); } [Fact] public void Order_SortsByLineThenColumn() { var (s1, e1) = UiText.Order(new UiText.Pos(2, 1), new UiText.Pos(0, 5)); Assert.Equal(new UiText.Pos(0, 5), s1); Assert.Equal(new UiText.Pos(2, 1), e1); var (s2, e2) = UiText.Order(new UiText.Pos(1, 8), new UiText.Pos(1, 2)); Assert.Equal(new UiText.Pos(1, 2), s2); Assert.Equal(new UiText.Pos(1, 8), e2); } // ── VOffset: vertical positioning for single-line mode ─────────────────── /// /// 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). /// [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); } /// /// 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. /// [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); } /// /// VJustify.Bottom: vertical offset = height - lineHeight - padding. /// [Fact] public void VOffset_Bottom_ReturnsHeightMinusLineHeightMinusPadding() { float y = UiText.VOffset(height: 55f, lineHeight: 12f, padding: 2f, VJustify.Bottom); Assert.Equal(55f - 12f - 2f, y); } /// /// 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. /// [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); } }