diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 56e6a49..530349b 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -20,6 +20,7 @@ public static class MenuKeys public const string MenuKeyMessageBox = "MessageBox"; public const string MenuKeyNavigation = "Navigation"; public const string MenuKeyNavMenu = "NavMenu"; + public const string MenuKeyNumberDisplayer = "NumberDisplayer"; public const string MenuKeyNumericUpDown = "NumericUpDown"; public const string MenuKeyPagination = "Pagination"; public const string MenuKeyRangeSlider = "RangeSlider"; diff --git a/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml new file mode 100644 index 0000000..c550a8c --- /dev/null +++ b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs new file mode 100644 index 0000000..35df3ef --- /dev/null +++ b/demo/Ursa.Demo/Pages/NumberDisplayerDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class NumberDisplayerDemo : UserControl +{ + public NumberDisplayerDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 43166bd..d8c0cb4 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -42,6 +42,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(), MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(), MenuKeys.MenuKeyNavMenu => new NavMenuDemoViewModel(), + MenuKeys.MenuKeyNumberDisplayer => new NumberDisplayerDemoViewModel(), MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(), MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(), MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 6f6da25..c24db24 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -29,7 +29,8 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" }, new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" }, new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "WIP"}, - new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" }, + new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer, Status = "New" }, + new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" }, new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination }, new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"}, new() { MenuHeader = "Selection List", Key = MenuKeys.MenuKeySelectionList, Status = "New" }, diff --git a/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs new file mode 100644 index 0000000..befdd5b --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/NumberDisplayerDemoViewModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Ursa.Demo.ViewModels; + +public partial class NumberDisplayerDemoViewModel: ObservableObject +{ + [ObservableProperty] private int _value; + [ObservableProperty] private double _doubleValue; + [ObservableProperty] private DateTime _dateValue; + public ICommand IncreaseCommand { get; } + public NumberDisplayerDemoViewModel() + { + IncreaseCommand = new RelayCommand(OnChange); + Value = 0; + DoubleValue = 0d; + DateValue = DateTime.Now; + } + + private void OnChange() + { + Random r = new Random(); + Value = r.Next(int.MaxValue); + DoubleValue = r.NextDouble() * 100000; + DateValue = DateTime.Today.AddDays(r.Next(1000)); + } +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml b/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml new file mode 100644 index 0000000..1a62aec --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/NumberDisplayer.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 0b2dae5..dee8aee 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -20,6 +20,7 @@ + diff --git a/src/Ursa/Controls/NumberDisplayer/Implementations.cs b/src/Ursa/Controls/NumberDisplayer/Implementations.cs new file mode 100644 index 0000000..a43fa0a --- /dev/null +++ b/src/Ursa/Controls/NumberDisplayer/Implementations.cs @@ -0,0 +1,73 @@ +using Avalonia.Animation; + +namespace Ursa.Controls; + +public class Int32Displayer : NumberDisplayer +{ + protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase); + + protected override InterpolatingAnimator GetAnimator() + { + return new IntAnimator(); + } + + private class IntAnimator : InterpolatingAnimator + { + public override int Interpolate(double progress, int oldValue, int newValue) + { + return oldValue + (int)((newValue - oldValue) * progress); + } + } + + protected override string GetString(int value) + { + return value.ToString(StringFormat); + } +} + +public class DoubleDisplayer : NumberDisplayer +{ + protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase); + + protected override InterpolatingAnimator GetAnimator() + { + return new DoubleAnimator(); + } + + private class DoubleAnimator : InterpolatingAnimator + { + public override double Interpolate(double progress, double oldValue, double newValue) + { + return oldValue + (newValue - oldValue) * progress; + } + } + + protected override string GetString(double value) + { + return value.ToString(StringFormat); + } +} + +public class DateDisplay : NumberDisplayer +{ + protected override Type StyleKeyOverride { get; } = typeof(NumberDisplayerBase); + + protected override InterpolatingAnimator GetAnimator() + { + return new DateAnimator(); + } + + private class DateAnimator : InterpolatingAnimator + { + public override DateTime Interpolate(double progress, DateTime oldValue, DateTime newValue) + { + var diff = (newValue - oldValue).TotalSeconds; + return oldValue + TimeSpan.FromSeconds(diff * progress); + } + } + + protected override string GetString(DateTime value) + { + return value.ToString(StringFormat); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/NumberDisplayer/NumberDisplayerBase.cs b/src/Ursa/Controls/NumberDisplayer/NumberDisplayerBase.cs new file mode 100644 index 0000000..25e817d --- /dev/null +++ b/src/Ursa/Controls/NumberDisplayer/NumberDisplayerBase.cs @@ -0,0 +1,133 @@ +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Styling; + +namespace Ursa.Controls; + +public abstract class NumberDisplayerBase : TemplatedControl +{ + public static readonly DirectProperty InternalTextProperty = AvaloniaProperty.RegisterDirect( + nameof(InternalText), o => o.InternalText, (o, v) => o.InternalText = v); + private string _internalText; + + internal string InternalText + { + get => _internalText; + set => SetAndRaise(InternalTextProperty, ref _internalText, value); + } + + public static readonly StyledProperty DurationProperty = AvaloniaProperty.Register( + nameof(Duration)); + + public TimeSpan Duration + { + get => GetValue(DurationProperty); + set => SetValue(DurationProperty, value); + } + + public static readonly StyledProperty StringFormatProperty = AvaloniaProperty.Register( + nameof(StringFormat)); + + public string StringFormat + { + get => GetValue(StringFormatProperty); + set => SetValue(StringFormatProperty, value); + } + + public static readonly StyledProperty IsSelectableProperty = AvaloniaProperty.Register( + nameof(IsSelectable)); + + public bool IsSelectable + { + get => GetValue(IsSelectableProperty); + set => SetValue(IsSelectableProperty, value); + } +} + +public abstract class NumberDisplayer: NumberDisplayerBase +{ + private Animation? _animation; + private CancellationTokenSource _cts = new (); + + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( + nameof(Value), defaultBindingMode:BindingMode.TwoWay); + + public T? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + private static readonly StyledProperty InternalValueProperty = AvaloniaProperty.Register, T?>( + nameof(InternalValue), defaultBindingMode:BindingMode.TwoWay); + + private T? InternalValue + { + get => GetValue(InternalValueProperty); + set => SetValue(InternalValueProperty, value); + } + + static NumberDisplayer() + { + ValueProperty.Changed.AddClassHandler, T?>((item, args) => + { + item.OnValueChanged(args.OldValue.Value, args.NewValue.Value); + }); + InternalValueProperty.Changed.AddClassHandler, T?>((item, args) => + { + item.InternalText = args.NewValue.Value is null ? string.Empty : item.GetString(args.NewValue.Value); + }); + DurationProperty.Changed.AddClassHandler, TimeSpan>((item, args) =>item.OnDurationChanged(args)); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _animation = new Animation + { + Duration = Duration, + FillMode = FillMode.Forward + }; + _animation.Children.Add(new KeyFrame() + { + Cue = new Cue(0.0), + Setters = { new Setter{Property = InternalValueProperty } } + }); + _animation.Children.Add(new KeyFrame() + { + Cue = new Cue(1.0), + Setters = { new Setter{Property = InternalValueProperty } } + }); + Animation.SetAnimator(_animation.Children[0].Setters[0], GetAnimator()); + Animation.SetAnimator(_animation.Children[1].Setters[0], GetAnimator()); + + // Display value directly to text on initialization in case value equals to default. + SetCurrentValue(InternalTextProperty, this.GetString(Value)); + } + + private void OnDurationChanged(AvaloniaPropertyChangedEventArgs args) + { + if (_animation is null) return; + _animation.Duration = args.NewValue.Value; + } + + private void OnValueChanged(T? oldValue, T? newValue) + { + if (_animation is null) + { + SetCurrentValue(InternalValueProperty, newValue); + return; + } + _cts.Cancel(); + _cts = new CancellationTokenSource(); + (_animation.Children[0].Setters[0] as Setter)!.Value = oldValue; + (_animation.Children[1].Setters[0] as Setter)!.Value = newValue; + _animation.RunAsync(this, _cts.Token); + } + + protected abstract InterpolatingAnimator GetAnimator(); + + protected abstract string GetString(T? value); +}