using System.Numerics; namespace AcDream.App.UI; /// /// Lightweight, data-bound string list for plugin markup. It owns only row /// selection and scroll position; the plugin binding remains the sole owner of /// rows and selected index. This deliberately avoids exposing App widget types /// through the BCL plugin contract. /// public sealed class UiMarkupList : UiElement { public Func> ItemsSource { get; set; } = static () => Array.Empty(); public Func> ItemColorsSource { get; set; } = static () => Array.Empty(); public Func SelectedIndexSource { get; set; } = static () => -1; public Action? SelectionChanged { get; set; } public UiDatFont? DatFont { get; set; } public float RowHeight { get; set; } = 18f; public float Padding { get; set; } = 3f; public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.92f); public Vector4 BorderColor { get; set; } = new(0.46f, 0.37f, 0.16f, 1f); public Vector4 TextColor { get; set; } = new(0.91f, 0.87f, 0.76f, 1f); public Vector4 SelectedColor { get; set; } = new(0.28f, 0.23f, 0.08f, 0.95f); private int _topRow; public override bool HandlesClick => true; protected override void OnDraw(UiRenderContext context) { IReadOnlyList items = ItemsSource(); IReadOnlyList itemColors = ItemColorsSource(); int visibleRows = VisibleRows; int selected = SelectedIndexSource(); if (selected >= 0 && selected < items.Count) { if (selected < _topRow) _topRow = selected; else if (selected >= _topRow + visibleRows) _topRow = selected - visibleRows + 1; } ClampTop(items.Count, visibleRows); context.DrawFill(0f, 0f, Width, Height, BackgroundColor); context.DrawRectOutline(0f, 0f, Width, Height, BorderColor, 1f); int end = Math.Min(items.Count, _topRow + visibleRows); for (int index = _topRow; index < end; index++) { float y = (index - _topRow) * RowHeight; if (index == selected) context.DrawFill(1f, y + 1f, Width - 2f, RowHeight - 1f, SelectedColor); string text = items[index]; Vector4 textColor = index < itemColors.Count ? Rgb(itemColors[index]) : TextColor; float textY = y + MathF.Max(0f, (RowHeight - (DatFont?.LineHeight ?? 14f)) * 0.5f); if (DatFont is { } font) context.DrawStringDat(font, text, Padding, textY, textColor, true); else context.DrawString(text, Padding, textY, textColor); } } public override bool OnEvent(in UiEvent e) { IReadOnlyList items = ItemsSource(); if (e.Type == UiEventType.Scroll) { _topRow -= Math.Sign(e.Data0); ClampTop(items.Count, VisibleRows); return true; } if (e.Type != UiEventType.MouseDown || !Enabled) return false; int row = (int)MathF.Floor(e.Data2 / MathF.Max(1f, RowHeight)); int index = _topRow + row; if (row >= 0 && row < VisibleRows && index >= 0 && index < items.Count) SelectionChanged?.Invoke(index); return true; } private int VisibleRows => Math.Max(1, (int)MathF.Floor( Height / MathF.Max(1f, RowHeight))); private void ClampTop(int count, int visibleRows) => _topRow = Math.Clamp(_topRow, 0, Math.Max(0, count - visibleRows)); private static Vector4 Rgb(uint value) => new( ((value >> 16) & 0xFFu) / 255f, ((value >> 8) & 0xFFu) / 255f, (value & 0xFFu) / 255f, 1f); }