feat: retail scrollbar chrome for markup <menu> overflow and <list> overflow
Owner live-client report 2026-09-07: "For scrollable dropdown or the meta window we use the same assets as we do in for example chat or inventory window." <menu> markup wiring (MarkupDocument.cs): a plugin <menu> is now always Scrollable (single-column, VTank HudCombo shape) instead of wrapping overflow into more grid columns, with PopupScrollbarHideWhenDisabled=true so the bar is entirely absent while the item count fits the "rows" window. RetailScrollbarChrome.ApplyToMenuPopup wires the same chrome ids the previous commit taught DrawScrollablePopupPlain to draw, for both style="plain" and style="retail" markup menus. <list> markup (UiMarkupList.cs / MarkupDocument.cs): a plugin <list> (single-column or <column> multi-column) that overflows its own row viewport now draws the retail scrollbar chrome at its right edge (VVS's own placement, 16px wide) instead of being wheel-scroll-only with no visible bar. The reserved 16px column only exists while rows actually overflow, in both column-layout modes (ComputeColumnLayout receives the already-shrunk width so the last/auto column absorbs the remainder correctly); the bar is fully interactive (up/down arrows, track paging, thumb drag) via a small UiScrollable projection kept in sync with the list's own _topRow, which stays the single source of truth. Wheel scrolling and a no-resolver hand-built list (draws nothing, no crash) are unchanged. Mutation shown to fail first: new UiMarkupListScrollbarTests/MarkupDocumentTests cases were written against pre-change UiMarkupList/MarkupDocument and failed (no scrollbar sprites ever emitted since UiMarkupList had no SpriteResolve property at all, and <menu> markup never set Scrollable) before the implementation landed; after: SingleColumn_Overflowing_DrawsRetailScrollbarChromeAtRightEdge and Columns_Overflowing_ReservesSixteenPixels_LastColumnShrinksAccordingly pin sprite ids + exact reserved-width geometry, *_ContentFits_DrawsNo(Scrollbar|ReservationLastColumnKeepsFullRemainder) pin the no-overflow/no-bar case, *_UpArrowClick_ScrollsUpByOneRow and ThumbDrag_MovesTopRowAndIsReadableByASubsequentClick pin interactivity via a following row click resolving to the moved position (mirroring MarkupListColumnsTests' own wheel-scroll pin), and the four new MarkupDocumentTests menu cases pin Scrollable/PopupScrollbarHideWhenDisabled/ the six chrome-id properties plus an end-to-end open-popup draw for both the overflowing (draws chrome) and non-overflowing (draws none) cases. Every pre-existing MarkupListColumnsTests/MarkupDocumentTests case stays green unchanged (none of their fixtures overflow their own viewport). docs/plugin-ui-markup.md updated: the <menu> style paragraph and a new <list> "Scrollbar" section describe the new chrome + auto-reservation, and the PITCH-transcription guidance is corrected to say the 16px scrollbar column is now automatic (no more manual fold-in/double-reservation advice). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
67aba8c386
commit
e9108277c4
5 changed files with 691 additions and 21 deletions
|
|
@ -1,3 +1,8 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
|
@ -368,4 +373,101 @@ public class MarkupDocumentTests
|
|||
Assert.Contains("menu", ex.Message);
|
||||
Assert.Contains("chrome", ex.Message);
|
||||
}
|
||||
|
||||
// ── Owner live-client report 2026-09-07 ("For scrollable dropdown or
|
||||
// the meta window we use the same assets as we do in for example chat
|
||||
// or inventory window"): a markup <menu> scrolls a single column
|
||||
// (rather than wrapping into grid columns) once it overflows its "rows"
|
||||
// window, and that popup's scrollbar draws the SAME chrome ids the chat
|
||||
// window/inventory scrollbar resolves through.
|
||||
|
||||
private sealed class OverflowMenuBinding
|
||||
{
|
||||
public IReadOnlyList<string> Choices { get; } =
|
||||
Enumerable.Range(0, 12).Select(i => $"row{i}").ToList();
|
||||
public string Selected { get; } = "row0";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_Markup_IsAlwaysScrollable_WithRetailScrollbarChromeWired()
|
||||
{
|
||||
var panel = MarkupDocument.Build(
|
||||
MenuXml(styleAttribute: ""), new MenuStyleBinding(), _ => (1u, 32, 32));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.True(menu.Scrollable);
|
||||
Assert.True(menu.PopupScrollbarHideWhenDisabled);
|
||||
Assert.Equal(RetailScrollbarChrome.Track, menu.ScrollTrackSprite);
|
||||
Assert.Equal(RetailScrollbarChrome.ThumbTopNormal, menu.ScrollThumbTopSprite);
|
||||
Assert.Equal(RetailScrollbarChrome.ThumbMidNormal, menu.ScrollThumbSprite);
|
||||
Assert.Equal(RetailScrollbarChrome.ThumbBotNormal, menu.ScrollThumbBottomSprite);
|
||||
Assert.Equal(RetailScrollbarChrome.UpNormal, menu.ScrollUpSprite);
|
||||
Assert.Equal(RetailScrollbarChrome.DownNormal, menu.ScrollDownSprite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_Markup_StyleRetail_IsAlsoScrollable_WithTheSameChrome()
|
||||
{
|
||||
var panel = MarkupDocument.Build(
|
||||
MenuXml(" style=\"retail\""), new MenuStyleBinding(), _ => (1u, 32, 32));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.True(menu.RetailButtonArt);
|
||||
Assert.True(menu.Scrollable);
|
||||
Assert.Equal(RetailScrollbarChrome.Track, menu.ScrollTrackSprite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_Markup_OverflowingItems_DrawsRetailScrollbarChrome_OnOpen()
|
||||
{
|
||||
var binding = new OverflowMenuBinding();
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\">" +
|
||||
"<menu x=\"4\" y=\"4\" w=\"120\" h=\"20\" items=\"{Choices}\" " +
|
||||
"selected=\"{Selected}\" openupward=\"false\"/>" +
|
||||
"</panel>";
|
||||
var panel = MarkupDocument.Build(xml, binding, id => (id, 8, 8));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
// Default rows=7, 12 items -> overflow.
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, Data1: 10, Data2: 10)));
|
||||
Assert.True(menu.IsOpen);
|
||||
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSourceForMenuTests(), "unused");
|
||||
renderer.Begin(new Vector2(200f, 200f));
|
||||
var ctx = new UiRenderContext(renderer, new Vector2(200f, 200f));
|
||||
menu.DrawOverlays(ctx);
|
||||
|
||||
int TrackQuads() => renderer.DebugSpriteSegmentVerts
|
||||
.Where(s => s.Texture == RetailScrollbarChrome.Track)
|
||||
.Sum(s => s.Verts.Count) / 48;
|
||||
Assert.True(TrackQuads() > 0, "expected the overflowing popup to draw the retail scrollbar track");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Menu_Markup_FewItems_DrawsNoScrollbarChrome_OnOpen()
|
||||
{
|
||||
var panel = MarkupDocument.Build(MenuXml(styleAttribute: ""), new MenuStyleBinding(), id => (id, 8, 8));
|
||||
var menu = Assert.IsType<UiMenu>(panel.Children[0]);
|
||||
|
||||
Assert.True(menu.OnEvent(new UiEvent(0, menu, UiEventType.MouseDown, Data1: 10, Data2: 10)));
|
||||
Assert.True(menu.IsOpen);
|
||||
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSourceForMenuTests(), "unused");
|
||||
renderer.Begin(new Vector2(200f, 200f));
|
||||
var ctx = new UiRenderContext(renderer, new Vector2(200f, 200f));
|
||||
menu.DrawOverlays(ctx);
|
||||
|
||||
int TrackQuads() => renderer.DebugSpriteSegmentVerts
|
||||
.Where(s => s.Texture == RetailScrollbarChrome.Track)
|
||||
.Sum(s => s.Verts.Count) / 48;
|
||||
Assert.Equal(0, TrackQuads());
|
||||
}
|
||||
|
||||
private sealed class NullGpuFrameSourceForMenuTests : ICurrentGpuFrameSource
|
||||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
302
tests/AcDream.App.Tests/UI/UiMarkupListScrollbarTests.cs
Normal file
302
tests/AcDream.App.Tests/UI/UiMarkupListScrollbarTests.cs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.App.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Owner live-client report 2026-09-07 ("For scrollable dropdown or the meta
|
||||
/// window we use the same assets as we do in for example chat or inventory
|
||||
/// window"): a plugin-markup <c><list></c> (<see cref="UiMarkupList"/>)
|
||||
/// that overflows its own row viewport now draws the same
|
||||
/// <see cref="RetailScrollbarChrome"/> sprites the chat SpewBox and the
|
||||
/// inventory <c>UiItemList</c> use, docked at the list's own right edge
|
||||
/// (VVS's own placement: 16px wide). Before this, an overflowing list only
|
||||
/// scrolled by mouse wheel with no visible bar at all.
|
||||
///
|
||||
/// <para>
|
||||
/// Covers: sprite emission gated on actual overflow (a fitting list emits
|
||||
/// none), the reserved 16px column shrink applying ONLY when the bar shows
|
||||
/// (both single-column and multi-column layout), and that the bar is fully
|
||||
/// interactive (up/down arrow clicks and a thumb drag both move
|
||||
/// <c>_topRow</c>, provable the same way <c>MarkupListColumnsTests</c>'
|
||||
/// own <c>Scroll_OffsetIsRespectedBySubsequentHitTests</c> proves wheel
|
||||
/// scrolling: read the moved position back through a subsequent row
|
||||
/// click/select).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiMarkupListScrollbarTests
|
||||
{
|
||||
private static (TextRenderer renderer, UiRenderContext ctx) MakeContext(float w, float h)
|
||||
{
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
||||
renderer.Begin(new Vector2(w, h));
|
||||
var ctx = new UiRenderContext(renderer, new Vector2(w, h));
|
||||
return (renderer, ctx);
|
||||
}
|
||||
|
||||
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
||||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
|
||||
private const int FloatsPerQuad = 48; // 6 vertices/quad x 8 floats/vertex (AppendQuad).
|
||||
|
||||
private static int QuadCount(
|
||||
System.Collections.Generic.IReadOnlyList<(uint Texture, System.Collections.Generic.IReadOnlyList<float> Verts)> segs,
|
||||
uint texture)
|
||||
=> segs.Where(s => s.Texture == texture).Sum(s => s.Verts.Count) / FloatsPerQuad;
|
||||
|
||||
// A distinctive resolver: every sprite id resolves to itself as the
|
||||
// texture (so QuadCount(segs, id) proves that EXACT chrome id drew),
|
||||
// with a non-zero native size so DrawTiled/DrawSprite never no-op.
|
||||
private static (uint tex, int w, int h) Resolve(uint id) => (id, 16, 16);
|
||||
|
||||
// ── Single-column mode ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SingleColumn_Overflowing_DrawsRetailScrollbarChromeAtRightEdge()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 visible rows
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
ItemsSource = () => Enumerable.Range(0, 10).Select(i => $"row{i}").ToArray(),
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
|
||||
Assert.Equal(1, QuadCount(segs, RetailScrollbarChrome.Track));
|
||||
Assert.Equal(1, QuadCount(segs, RetailScrollbarChrome.UpNormal));
|
||||
Assert.Equal(1, QuadCount(segs, RetailScrollbarChrome.DownNormal));
|
||||
|
||||
// Track drawn at the list's own right edge, reserving 16px, full height.
|
||||
var trackSeg = Assert.Single(segs, s => s.Texture == RetailScrollbarChrome.Track);
|
||||
Assert.Equal(84f, trackSeg.Verts[0], 2); // x = Width(100) - 16
|
||||
Assert.Equal(0f, trackSeg.Verts[1], 2);
|
||||
Assert.Equal(100f, trackSeg.Verts[8], 2); // x + w = Width
|
||||
Assert.Equal(40f, trackSeg.Verts[9], 2); // y + h = Height
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleColumn_ContentFits_DrawsNoScrollbarChromeAtAll()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 visible rows
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
ItemsSource = () => new[] { "only-one-row" },
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.Track));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.UpNormal));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.DownNormal));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.ThumbMidNormal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleColumn_NoSpriteResolveWired_DrawsNoScrollbar_NoCrash()
|
||||
{
|
||||
// A hand-built list with no host resolver (SpriteResolve stays null)
|
||||
// must not throw and must draw no chrome at all — matches the
|
||||
// pre-existing "wheel-only, no visible bar" contract for that case.
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f,
|
||||
SelectedIndexSource = () => -1,
|
||||
ItemsSource = () => Enumerable.Range(0, 10).Select(i => $"row{i}").ToArray(),
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
|
||||
// The background fill/border still draw (untextured, id 0) — only
|
||||
// the scrollbar chrome itself is gated on a resolver being wired.
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.Track));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.UpNormal));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.DownNormal));
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.ThumbMidNormal));
|
||||
Assert.DoesNotContain(segs, s => s.Texture != 0u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleColumn_UpArrowClick_ScrollsUpByOneRow()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 visible rows of 10
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
ItemsSource = () => Enumerable.Range(0, 10).Select(i => $"row{i}").ToArray(),
|
||||
};
|
||||
var (_, ctx) = MakeContext(200f, 200f);
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
// Scroll down 3 rows via wheel first (proven convention from
|
||||
// MarkupListColumnsTests), then click the up arrow once and confirm
|
||||
// a row click resolves one row higher.
|
||||
for (int i = 0; i < 3; i++)
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.Scroll, Data0 = -1 });
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
int? selected = null;
|
||||
list.SelectionChanged = row => selected = row;
|
||||
|
||||
// Up-arrow button occupies the scrollbar's own top 16px, x in
|
||||
// [84,100).
|
||||
Assert.True(list.OnEvent(new UiEvent
|
||||
{
|
||||
Type = UiEventType.MouseDown, Data1 = 90, Data2 = 5,
|
||||
}));
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
// Row 0 of the (now one-row-higher) view is absolute row 2.
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 10, Data2 = 2 });
|
||||
Assert.Equal(2, selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleColumn_ThumbDrag_MovesTopRowAndIsReadableByASubsequentClick()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 of 10 rows visible
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
ItemsSource = () => Enumerable.Range(0, 10).Select(i => $"row{i}").ToArray(),
|
||||
};
|
||||
var (_, ctx) = MakeContext(200f, 200f);
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
// Track spans y in [16,24) (Height 40 - 16 up - 16 down = 8px track,
|
||||
// thumb ratio 2/10=0.2 but floored to the 8px MinThumb). Press
|
||||
// squarely inside the thumb (drawn at the very top initially) then
|
||||
// drag to the bottom of the track to scroll to the end.
|
||||
Assert.True(list.OnEvent(new UiEvent
|
||||
{
|
||||
Type = UiEventType.MouseDown, Data1 = 90, Data2 = 18,
|
||||
}));
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseMove, Data1 = 90, Data2 = 40 });
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseUp, Data1 = 90, Data2 = 40 });
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
int? selected = null;
|
||||
list.SelectionChanged = row => selected = row;
|
||||
// Click the FIRST visible row after dragging to the end — must
|
||||
// resolve to the last possible top row (10-2=8), not row 0.
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 10, Data2 = 2 });
|
||||
Assert.Equal(8, selected);
|
||||
}
|
||||
|
||||
// ── Multi-column mode ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Columns_Overflowing_ReservesSixteenPixels_LastColumnShrinksAccordingly()
|
||||
{
|
||||
// 100px-wide list, one fixed 20px column + one auto (last) column
|
||||
// that would otherwise absorb 80px; with the bar reserved it must
|
||||
// absorb only 80-16=64px.
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 visible rows
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
Columns = new[]
|
||||
{
|
||||
UiMarkupListColumn.Text(20f, () => Enumerable.Range(0, 10).Select(i => $"a{i}").ToArray(), null),
|
||||
UiMarkupListColumn.Icon(
|
||||
0f, () => Enumerable.Range(0, 10).Select(i => (uint)(i + 1)).ToArray(),
|
||||
id => (id, 16, 16), _ => { }),
|
||||
},
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
|
||||
Assert.Equal(1, QuadCount(segs, RetailScrollbarChrome.Track));
|
||||
|
||||
// The icon column's cell now runs [20,84) (100-16 scrollbar): 62px
|
||||
// usable extent (cellW-2), scale=1 (16px icon fits), centered ->
|
||||
// x = 20 + 1 + (62-16)/2 = 44.
|
||||
var iconQuad = Assert.Single(segs, s => s.Texture == 1u);
|
||||
Assert.Equal(44f, iconQuad.Verts[0], 2);
|
||||
Assert.True(iconQuad.Verts[8] <= 84f + 0.01f,
|
||||
$"expected the icon column's cell to shrink for the reserved scrollbar, got right edge {iconQuad.Verts[8]}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_ContentFits_NoReservation_LastColumnKeepsFullRemainder()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 visible rows, 1 row of data
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
Columns = new[]
|
||||
{
|
||||
UiMarkupListColumn.Text(20f, () => new[] { "a" }, null),
|
||||
UiMarkupListColumn.Icon(0f, () => new uint[] { 1u }, id => (id, 16, 16), _ => { }),
|
||||
},
|
||||
};
|
||||
var (renderer, ctx) = MakeContext(200f, 200f);
|
||||
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
var segs = renderer.DebugSpriteSegmentVerts;
|
||||
|
||||
Assert.Equal(0, QuadCount(segs, RetailScrollbarChrome.Track));
|
||||
// No reservation: the cell is the full [20,100) 80px (78px usable
|
||||
// extent), scale still clamps to 1 (16px icon), but centered in the
|
||||
// WIDER cell it lands further right than the reserved case's x=44:
|
||||
// x = 20 + 1 + (78-16)/2 = 52.
|
||||
var iconQuad = Assert.Single(segs, s => s.Texture == 1u);
|
||||
Assert.Equal(52f, iconQuad.Verts[0], 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_Overflowing_UpArrowClick_ScrollsUpByOneRow()
|
||||
{
|
||||
var list = new UiMarkupList
|
||||
{
|
||||
Width = 100f, Height = 40f, RowHeight = 18f, // 2 of 10 rows visible
|
||||
SpriteResolve = Resolve,
|
||||
SelectedIndexSource = () => -1,
|
||||
Columns = new[]
|
||||
{
|
||||
UiMarkupListColumn.Text(100f, () => Enumerable.Range(0, 10).Select(i => $"a{i}").ToArray(), null),
|
||||
},
|
||||
};
|
||||
var (_, ctx) = MakeContext(200f, 200f);
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.Scroll, Data0 = -1 });
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
int? selected = null;
|
||||
list.SelectionChanged = row => selected = row;
|
||||
|
||||
Assert.True(list.OnEvent(new UiEvent
|
||||
{
|
||||
Type = UiEventType.MouseDown, Data1 = 90, Data2 = 5,
|
||||
}));
|
||||
list.DrawSelfAndChildren(ctx);
|
||||
|
||||
list.OnEvent(new UiEvent { Type = UiEventType.MouseDown, Data1 = 10, Data2 = 2 });
|
||||
Assert.Equal(2, selected);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue