17 private const int ContentPadding = 2;
18 private const int DefaultItemHeight = 24;
19 private const int ScrollBarWidth = 12;
20 private const int ScrollBarMargin = 3;
21 private const int MinimumScrollBarThumbHeight = 18;
23 private readonly List<string> _items = [];
24 private readonly List<TextBlock> _rowTextBlocks = [];
26 private int _selectedIndex = -1;
27 private int _topIndex;
28 private int _itemHeight = DefaultItemHeight;
29 private int _baseZOrder;
30 private int _mouseWheelScrollItems = 3;
32 private bool _isDraggingScrollBarThumb;
33 private float _scrollBarDragOffset;
34 private int? _scrollBarPointerId;
35 private bool _suppressScrollBarClick;
58 IEnumerable<string>? items =
null,
59 string? nickname =
null)
62 ArgumentNullException.ThrowIfNull(view);
64 Background = CreateBackground(renderSurfaceHost, view, bounds);
65 SelectionHighlight = CreateSelectionHighlight(renderSurfaceHost, view, GetInitialSelectionBounds(bounds));
74 CompleteInitialization(items);
88 IEnumerable<string>? items =
null,
89 string? nickname =
null)
92 ArgumentNullException.ThrowIfNull(sceneLayer);
94 Background = CreateBackground(renderSurfaceHost, sceneLayer, bounds);
95 SelectionHighlight = CreateSelectionHighlight(renderSurfaceHost, sceneLayer, GetInitialSelectionBounds(bounds));
104 CompleteInitialization(items);
126 public IReadOnlyList<string>
Items => _items;
132 ? Background.ScreenBounds
144 throw new ArgumentOutOfRangeException(nameof(value),
"Item height must be at least 12 pixels.");
146 if (_itemHeight == value)
151 EnsureSelectionVisible();
164 get => _verticalScrollBarVisibility;
167 if (!Enum.IsDefined(value))
168 throw new ArgumentOutOfRangeException(nameof(value));
170 if (_verticalScrollBarVisibility == value)
173 _verticalScrollBarVisibility = value;
187 get => _mouseWheelScrollItems;
188 set => _mouseWheelScrollItems = value > 0
190 :
throw new ArgumentOutOfRangeException(nameof(value),
"Mouse-wheel scrolling must advance at least one item.");
204 int clamped = Math.Clamp(value, 0, GetMaximumTopIndex());
205 if (_topIndex == clamped)
218 get => _selectedIndex;
225 public string?
SelectedItem => _selectedIndex >= 0 && _selectedIndex < _items.Count
226 ? _items[_selectedIndex]
236 ArgumentNullException.ThrowIfNull(items);
238 bool hadSelection = _selectedIndex >= 0;
239 string[] replacement = items.Select(
static item => item ??
string.Empty).ToArray();
241 _items.AddRange(replacement);
259 _items.
Add(item ??
string.Empty);
272 if (index < 0 || index >= _items.Count)
273 throw new ArgumentOutOfRangeException(nameof(index));
275 int oldSelectedIndex = _selectedIndex;
276 bool selectedItemWasRemoved = index == oldSelectedIndex;
278 _items.RemoveAt(index);
280 if (_items.Count == 0)
284 else if (index < oldSelectedIndex)
286 _selectedIndex = oldSelectedIndex - 1;
288 else if (selectedItemWasRemoved)
290 _selectedIndex = Math.Min(index, _items.Count - 1);
294 EnsureSelectionVisible();
297 if (_selectedIndex != oldSelectedIndex || selectedItemWasRemoved)
309 bool hadSelection = _selectedIndex >= 0;
328 if (index < -1 || index >= _items.Count)
329 throw new ArgumentOutOfRangeException(nameof(index));
331 if (_selectedIndex == index)
333 EnsureSelectionVisible();
337 _selectedIndex = index;
338 EnsureSelectionVisible();
339 RefreshSelectionHighlight();
350 if (_items.Count == 0)
353 int next = _selectedIndex < 0 ? 0 : Math.Min(_selectedIndex + 1, _items.Count - 1);
363 if (_items.Count == 0)
366 int previous = _selectedIndex < 0 ? 0 : Math.Max(_selectedIndex - 1, 0);
377 _baseZOrder = zOrder;
378 Background.ZOrder = zOrder;
379 SelectionHighlight.ZOrder = zOrder + 1;
381 foreach (
TextBlock row
in _rowTextBlocks)
382 row.ZOrder = zOrder + 2;
384 VerticalScrollBarTrack.ZOrder = zOrder + 3;
385 VerticalScrollBarThumb.ZOrder = zOrder + 4;
407 RefreshSelectionHighlight();
413 _isDraggingScrollBarThumb =
false;
414 _scrollBarPointerId =
null;
415 _suppressScrollBarClick =
false;
416 base.ProcessHidden();
422 base.OnPointerClick(args);
427 if (_suppressScrollBarClick || IsPointerOverScrollBar(args))
429 _suppressScrollBarClick =
false;
434 RectangleF screenBounds =
Background.GetDrawLocationScreen(args.
View);
436 if (screenBounds.Height <= 0f ||
Bounds.Height <= 0)
439 float scaleY = screenBounds.Height /
Bounds.Height;
440 float localY = args.ScreenPositionPx.Y - screenBounds.Top - ContentPadding * scaleY;
443 if (localY < 0f || rowHeightScreen <= 0f)
446 int row = (int)(localY / rowHeightScreen);
460 base.OnPointerDown(args);
462 if (_scrollBarPointerId.HasValue)
465 _suppressScrollBarClick =
false;
470 PointF local = GetLocalPointerPosition(args);
471 Rectangle thumbBounds = GetScrollBarThumbBounds(
Bounds);
472 Rectangle trackBounds = GetScrollBarTrackBounds(
Bounds);
474 if (!trackBounds.Contains(Point.Round(local)))
479 _suppressScrollBarClick =
true;
480 if (thumbBounds.Contains(Point.Round(local)))
482 _isDraggingScrollBarThumb =
true;
483 _scrollBarDragOffset = local.Y - thumbBounds.Top;
493 base.OnPointerMove(args);
498 Rectangle trackBounds = GetScrollBarTrackBounds(
Bounds);
499 Rectangle thumbBounds = GetScrollBarThumbBounds(
Bounds);
500 float travel = trackBounds.Height - thumbBounds.Height;
502 if (travel <= 0f || GetMaximumTopIndex() == 0)
505 float desiredTop = GetLocalPointerPosition(args).Y - _scrollBarDragOffset;
506 float fraction = Math.Clamp((desiredTop - trackBounds.Top) / travel, 0f, 1f);
507 TopIndex = (int)MathF.Round(fraction * GetMaximumTopIndex());
514 base.OnPointerUp(args);
516 if (_scrollBarPointerId == args.
PointerId)
518 _isDraggingScrollBarThumb =
false;
519 _scrollBarPointerId =
null;
527 base.OnMouseWheel(args);
529 if (args.
Delta == 0 || GetMaximumTopIndex() == 0)
532 long notches = Math.Max(1, Math.Abs((
long)args.
Delta) / 120);
541 base.OnKeyboardInput(args);
543 if (_items.Count == 0)
586 private void CompleteInitialization(IEnumerable<string>? items)
588 SelectionHighlight.Visible =
false;
589 VerticalScrollBarTrack.Visible =
false;
590 VerticalScrollBarThumb.Visible =
false;
595 if (items is not
null)
596 _items.AddRange(items.Select(
static item => item ??
string.Empty));
601 private void EnsureSelectionVisible()
603 if (_selectedIndex < 0)
606 if (_selectedIndex < _topIndex)
608 _topIndex = _selectedIndex;
614 if (_selectedIndex > lastVisible)
616 _topIndex = Math.Min(GetMaximumTopIndex(), _selectedIndex -
VisibleItemCount + 1);
621 private void ClampTopIndex()
623 _topIndex = Math.Clamp(_topIndex, 0, GetMaximumTopIndex());
626 private int GetMaximumTopIndex()
631 private void RefreshRows()
637 foreach (TextBlock row
in _rowTextBlocks)
643 _rowTextBlocks.Clear();
648 for (
int rowIndex = 0; rowIndex < count; rowIndex++)
650 int itemIndex =
TopIndex + rowIndex;
651 Rectangle rowBounds = GetRowBounds(bounds, rowIndex);
652 TextBlock row = CreateRowText(rowBounds, _items[itemIndex]);
653 row.ZOrder = _baseZOrder + 2;
655 _rowTextBlocks.Add(row);
658 RefreshSelectionHighlight();
664 private TextBlock CreateRowText(Rectangle bounds,
string text)
666 TextBlock row =
Mode == DirectDrawingMode.View
671 .SetFont(SKTypeface.Default, 15f, minSize: 9f)
672 .
SetColors(SKColors.White, SKColors.Transparent)
673 .
SetAlignment(SKTextAlign.Left, TextBlock.VerticalAlign.Center)
678 private void RefreshSelectionHighlight()
682 SelectionHighlight.Visible =
false;
686 int rowIndex = _selectedIndex -
TopIndex;
689 ? SelectionHighlight.ScreenBounds
691 Rectangle resizedBounds =
new(currentBounds.Location, selectionBounds.Size);
694 SelectionHighlight.ScreenBounds = resizedBounds;
696 SelectionHighlight.WorldBounds = resizedBounds;
700 new Vector2(ContentPadding, ContentPadding + rowIndex *
ItemHeight));
702 SelectionHighlight.Visible =
true;
705 private Rectangle GetRowBounds(Rectangle bounds,
int rowIndex)
707 int y = bounds.Y + ContentPadding + rowIndex *
ItemHeight;
708 return new Rectangle(bounds.X + ContentPadding,
710 Math.Max(1, bounds.Width - ContentPadding * 2 - (NeedsVerticalScrollBar ? ScrollBarWidth + ScrollBarMargin * 2 : 0)),
711 Math.Min(
ItemHeight, Math.Max(1, bounds.Bottom - ContentPadding - y)));
714 private void RefreshScrollBar()
717 VerticalScrollBarTrack.Visible = visible;
718 VerticalScrollBarThumb.Visible = visible;
729 private Rectangle GetScrollBarTrackBounds(Rectangle bounds)
731 return new Rectangle(bounds.Right - ScrollBarMargin - ScrollBarWidth,
732 bounds.Top + ContentPadding + ScrollBarMargin,
734 Math.Max(1, bounds.Height - (ContentPadding + ScrollBarMargin) * 2));
737 private Rectangle GetScrollBarThumbBounds(Rectangle bounds)
739 Rectangle trackBounds = GetScrollBarTrackBounds(bounds);
740 int thumbHeight = _items.Count == 0
742 : Math.Clamp((
int)MathF.Round(trackBounds.Height * Math.Min(1f, (
float)
VisibleItemCount / _items.Count)),
743 Math.Min(MinimumScrollBarThumbHeight, trackBounds.Height),
745 int travel = trackBounds.Height - thumbHeight;
746 int maximumTopIndex = GetMaximumTopIndex();
747 int offset = maximumTopIndex == 0 ? 0 : (int)MathF.Round(travel * ((
float)
TopIndex / maximumTopIndex));
748 return new Rectangle(trackBounds.Left, trackBounds.Top + offset, trackBounds.Width, thumbHeight);
751 private PointF GetLocalPointerPosition(WidgetPointerEventArgs args)
753 RectangleF screenBounds =
Background.GetDrawLocationScreen(args.View);
754 screenBounds.Offset(args.WrappedOffsetWorldPx.X * args.View.Viewport.Zoom,
755 args.WrappedOffsetWorldPx.Y * args.View.Viewport.Zoom);
756 float scaleX = screenBounds.Width /
Bounds.Width;
757 float scaleY = screenBounds.Height /
Bounds.Height;
758 return new PointF(
Bounds.Left + (args.ScreenPositionPx.X - screenBounds.Left) / scaleX,
759 Bounds.Top + (args.ScreenPositionPx.Y - screenBounds.Top) / scaleY);
762 private bool IsPointerOverScrollBar(WidgetPointerEventArgs args)
768 private void SetRectangleBounds(DirectRectangle rectangle, Rectangle bounds)
771 rectangle.ScreenBounds = bounds;
773 rectangle.WorldBounds = bounds;
778 private static DirectRectangle CreateBackground(RenderSurfaceHostBase host,
782 return ConfigureBackground(
new DirectRectangle(Color.FromArgb(255, 36, 36, 44), host, view, bounds));
785 private static DirectRectangle CreateBackground(RenderSurfaceHostBase host,
789 return ConfigureBackground(
new DirectRectangle(Color.FromArgb(255, 36, 36, 44), host, layer, bounds));
792 private static DirectRectangle ConfigureBackground(DirectRectangle background)
800 private static DirectRectangle CreateSelectionHighlight(RenderSurfaceHostBase host,
804 return ConfigureSelectionHighlight(
new DirectRectangle(Color.FromArgb(220, 63, 91, 138), host, view, bounds));
807 private static DirectRectangle CreateSelectionHighlight(RenderSurfaceHostBase host,
811 return ConfigureSelectionHighlight(
new DirectRectangle(Color.FromArgb(220, 63, 91, 138), host, layer, bounds));
814 private static DirectRectangle ConfigureSelectionHighlight(DirectRectangle highlight)
821 private static DirectRectangle CreateScrollBarTrack(RenderSurfaceHostBase host,
View view, Rectangle bounds)
823 return ConfigureScrollBarTrack(
new DirectRectangle(Color.FromArgb(255, 26, 26, 32), host, view, bounds));
826 private static DirectRectangle CreateScrollBarTrack(RenderSurfaceHostBase host,
SceneLayer layer, Rectangle bounds)
828 return ConfigureScrollBarTrack(
new DirectRectangle(Color.FromArgb(255, 26, 26, 32), host, layer, bounds));
831 private static DirectRectangle ConfigureScrollBarTrack(DirectRectangle track)
836 private static DirectRectangle CreateScrollBarThumb(RenderSurfaceHostBase host,
View view, Rectangle bounds)
838 return ConfigureScrollBarThumb(
new DirectRectangle(Color.FromArgb(255, 126, 126, 142), host, view, bounds));
841 private static DirectRectangle CreateScrollBarThumb(RenderSurfaceHostBase host,
SceneLayer layer, Rectangle bounds)
843 return ConfigureScrollBarThumb(
new DirectRectangle(Color.FromArgb(255, 126, 126, 142), host, layer, bounds));
846 private static DirectRectangle ConfigureScrollBarThumb(DirectRectangle thumb)
851 private static Rectangle GetInitialSelectionBounds(Rectangle bounds)
853 return new Rectangle(bounds.X + ContentPadding,
854 bounds.Y + ContentPadding,
855 Math.Max(1, bounds.Width - ContentPadding * 2),
859 private static void Refresh(DirectRectangle rectangle)
864 private static Rectangle ValidateBounds(Rectangle bounds)
866 if (bounds.Width < 24 || bounds.Height < DefaultItemHeight + ContentPadding * 2)
867 throw new ArgumentOutOfRangeException(nameof(bounds), bounds,
"List-box bounds are too small.");