From c5e56a0c31092285d8dd38f79bddeefe51f186c1 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Apr 2024 22:49:03 +0800 Subject: [PATCH] feat: add panel implementations. --- .../DateTimePicker/UrsaTimePickerPanel.cs | 276 +++++++++++++++++- 1 file changed, 274 insertions(+), 2 deletions(-) diff --git a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs index 5f8397c..e38d549 100644 --- a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs +++ b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs @@ -1,16 +1,64 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.VisualTree; namespace Ursa.Controls.Panels; +public enum TimePickerPanelType +{ + Hour, + Minute, + Second, + TimePeriod // AM/PM +} /// /// The panel to display items for time selection /// public class UrsaTimePickerPanel: Panel, ILogicalScrollable { + private ScrollContentPresenter? _parentScroller; + private double _extendOne; + private Vector _offset; + private bool _initialized; + private int _countItemAboveBelowSelected; + + public Vector Offset + { + get => _offset; + set => SetOffset(value); + } + + private int _increment; + + public int Increment + { + get => _increment; + set => _increment = value; + } + + private int _selectedIndex; + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + } + } + + private int _selectedValue; + + public int SelectedValue + { + get => _selectedValue; + set => _selectedValue = value; + } + public static readonly StyledProperty ItemHeightProperty = AvaloniaProperty.Register( nameof(ItemHeight), defaultValue: 32); @@ -46,9 +94,232 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable _pageScrollSize = new Size(0, newValue * 3); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = this.GetVisualParent() as ScrollContentPresenter; + _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = null; + } + + public override void ApplyTemplate() + { + base.ApplyTemplate(); + AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble); + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (e.Source is Visual source && GetItemFromSource(source) is { Tag: int tag }) + { + SelectedValue = tag; + e.Handled = true; + } + } + + private ListBoxItem? GetItemFromSource(Visual source) + { + var item = source; + while (item != null && !(item is ListBoxItem)) + { + item = item.GetVisualParent(); + } + return item as ListBoxItem; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) + throw new InvalidOperationException("Panel must have finite height"); + if(_initialized) UpdateHelperInfo(); + double initY = availableSize.Height / 2.0 - ItemHeight / 2.0; + _countItemAboveBelowSelected = (int)Math.Ceiling(initY / ItemHeight); + + var children = Children; + + CreateOrDestroyItems(children); + + for(int i = 0; i< children.Count; i++) + { + children[i].Measure(availableSize); + } + + if (!_initialized) + { + UpdateItems(); + RaiseScrollInvalidated(System.EventArgs.Empty); + _initialized = true; + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (Children.Count == 0) + { + return base.ArrangeOverride(finalSize); + } + + var itemHeight = ItemHeight; + var children = Children; + Rect rc; + double initY = finalSize.Height / 2.0 - itemHeight / 2.0; + + if (ShouldLoop) + { + var currentSet = Math.Truncate(Offset.Y / _extendOne); + initY += _extendOne * currentSet + (_selectedIndex - _countItemAboveBelowSelected) * ItemHeight; + foreach (var child in children) + { + rc = new Rect(0, initY-Offset.Y, finalSize.Width, itemHeight); + child.Arrange(rc); + initY += itemHeight; + } + } + else + { + var first = Math.Max(0, _selectedIndex - _countItemAboveBelowSelected); + foreach (var child in children) + { + rc = new Rect(0, initY+first+itemHeight-Offset.Y, finalSize.Width, itemHeight); + child.Arrange(rc); + initY += itemHeight; + } + } + return finalSize; + } + + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + { + var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; + if(!snapY.Equals(Offset.Y)) + { + Offset = Offset.WithY(snapY); + } + } + + private void SetOffset(Vector value) + { + var oldValue = _offset; + _offset = value; + var dy = _offset.Y - oldValue.Y; + var children = this.Children; + // TODO + } + + private int _range; + private int _maximumValue; + private int _minimumValue; + private int _totalItems; + private void UpdateHelperInfo() + { + _range = _maximumValue - _minimumValue + 1; + _totalItems = (int)Math.Ceiling((double)_range / _increment); + var itemHeight = ItemHeight; + _extent = new Size(0, ShouldLoop ? _totalItems * itemHeight * 100 : _totalItems * itemHeight); + + _extendOne = _totalItems * itemHeight; + _offset = new Vector(0, + ShouldLoop ? _extendOne * 50 + _selectedIndex * itemHeight : _selectedIndex * itemHeight); + } + + private void UpdateItems() + { + var children = Children; + var min = _minimumValue; + var max = _maximumValue; + var selected = SelectedValue; + + int first; + if (ShouldLoop) + { + first = (_selectedIndex - _countItemAboveBelowSelected) % _totalItems; + first = first < 0 ? min + (first + _totalItems) * Increment : min + first * Increment; + } + else + { + first = min + Math.Max(0, _selectedIndex - _countItemAboveBelowSelected) * Increment; + } + + for (int i = 0; i < children.Count; i++) + { + ListBoxItem item = (ListBoxItem)children[i]; + item.Content = first + i * Increment; // TODO + item.Tag = first; + item.IsSelected = first == selected; + first += Increment; + if(first > max) + { + first = min; + } + } + + } + + private void CreateOrDestroyItems(Avalonia.Controls.Controls children) + { + int totalItemsInViewport = _countItemAboveBelowSelected * 2 + 1; + if (!ShouldLoop) + { + int numItemAboveSelect = _countItemAboveBelowSelected; + if (_selectedIndex - _countItemAboveBelowSelected < 0) + { + numItemAboveSelect = _selectedIndex; + } + int numItemBelowSelect = _countItemAboveBelowSelected; + if (_selectedIndex + _countItemAboveBelowSelected >= _totalItems) + { + numItemBelowSelect = _totalItems - _selectedIndex - 1; + } + totalItemsInViewport = numItemAboveSelect + numItemBelowSelect + 1; + } + + while (children.Count totalItemsInViewport) + { + var countToRemove = children.Count - totalItemsInViewport; + children.RemoveRange(children.Count-countToRemove, countToRemove); + } + } + + private int CoerceSelected(int newValue) + { + if(newValue < _minimumValue) + { + return _minimumValue; + } + if(newValue > _maximumValue) + { + return _maximumValue; + } + if (newValue % _increment == 0) return newValue; + var items = Enumerable.Range(_minimumValue, _range).Where(x => x % _increment == 0).ToList(); + var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x); + return items.IndexOf(nearest) * Increment; + } public event EventHandler? OnSelectionChanged; - public Size Extent { get; private set; } - public Vector Offset { get; set; } + private Size _extent; + public Size Extent { + get => _extent; + private set => _extent = value; + } public Size Viewport => Bounds.Size; public bool BringIntoView(Control target, Rect targetRect) => false; public Control? GetControlInDirection(NavigationDirection direction, Control? from) => null; @@ -59,4 +330,5 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable public Size ScrollSize => _scrollSize; public Size PageScrollSize => _pageScrollSize; public event EventHandler? ScrollInvalidated; + public event EventHandler? SelectionChanged; } \ No newline at end of file