diff --git a/demo/Ursa.Demo/Pages/ClockDemo.axaml b/demo/Ursa.Demo/Pages/ClockDemo.axaml new file mode 100644 index 0000000..892aa17 --- /dev/null +++ b/demo/Ursa.Demo/Pages/ClockDemo.axaml @@ -0,0 +1,18 @@ + + + + + diff --git a/demo/Ursa.Demo/Pages/ClockDemo.axaml.cs b/demo/Ursa.Demo/Pages/ClockDemo.axaml.cs new file mode 100644 index 0000000..058136a --- /dev/null +++ b/demo/Ursa.Demo/Pages/ClockDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class ClockDemo : UserControl +{ + public ClockDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/ClockDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ClockDemoViewModel.cs new file mode 100644 index 0000000..48cda91 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/ClockDemoViewModel.cs @@ -0,0 +1,31 @@ +using System; +using System.Timers; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public partial class ClockDemoViewModel: ObservableObject, IDisposable +{ + private Timer _timer; + + [ObservableProperty] private DateTime _time; + public ClockDemoViewModel() + { + Time = DateTime.Now; + _timer = new Timer(1000); + _timer.Elapsed += TimerOnElapsed; + _timer.Start(); + } + + private void TimerOnElapsed(object? sender, ElapsedEventArgs e) + { + Time = DateTime.Now; + } + + public void Dispose() + { + _timer.Stop(); + _timer.Elapsed -= TimerOnElapsed; + _timer.Dispose(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 47eec8a..1ac7c29 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -30,6 +30,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(), MenuKeys.MenuKeyBreadcrumb => new BreadcrumbDemoViewModel(), MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(), + MenuKeys.MenuKeyClock => new ClockDemoViewModel(), MenuKeys.MenuKeyDialog => new DialogDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), MenuKeys.MenuKeyDisableContainer => new DisableContainerDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 62d76a7..c58784d 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -17,6 +17,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Breadcrumb", Key = MenuKeys.MenuKeyBreadcrumb, Status = "New" }, new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated" }, new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput }, + new() { MenuHeader = "Clock", Key = MenuKeys.MenuKeyClock, Status = "New" }, new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog }, new() { MenuHeader = "Disable Container", Key = MenuKeys.MenuKeyDisableContainer }, new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider }, @@ -59,6 +60,7 @@ public static class MenuKeys public const string MenuKeyButtonGroup = "ButtonGroup"; public const string MenuKeyBreadcrumb= "Breadcrumb"; public const string MenuKeyClassInput = "Class Input"; + public const string MenuKeyClock = "Clock"; public const string MenuKeyDialog = "Dialog"; public const string MenuKeyDivider = "Divider"; public const string MenuKeyDisableContainer = "DisableContainer"; diff --git a/src/Ursa.Themes.Semi/Controls/Clock.axaml b/src/Ursa.Themes.Semi/Controls/Clock.axaml new file mode 100644 index 0000000..58809b8 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/Clock.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index d05cb2e..0e6694b 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -6,6 +6,7 @@ + diff --git a/src/Ursa.Themes.Semi/Converters/ClockHandLengthConverter.cs b/src/Ursa.Themes.Semi/Converters/ClockHandLengthConverter.cs new file mode 100644 index 0000000..6715eef --- /dev/null +++ b/src/Ursa.Themes.Semi/Converters/ClockHandLengthConverter.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Ursa.Themes.Semi.Converters; + +public class ClockHandLengthConverter(double ratio) : IValueConverter +{ + public static ClockHandLengthConverter Hour { get; } = new(1-0.618); + public static ClockHandLengthConverter Minute { get; } = new(0.618); + public static ClockHandLengthConverter Second { get; } = new(1); + + private double _ratio = ratio; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is double d) + { + return d * ratio / 2; + } + return 0.0; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Clock/Clock.cs b/src/Ursa/Controls/Clock/Clock.cs new file mode 100644 index 0000000..f73b0fd --- /dev/null +++ b/src/Ursa/Controls/Clock/Clock.cs @@ -0,0 +1,141 @@ +using Avalonia; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Ursa.Controls; + +[TemplatePart(PART_ClockTicks, typeof(ClockTicks))] +public class Clock: TemplatedControl +{ + public const string PART_ClockTicks = "PART_ClockTicks"; + + public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( + nameof(Time), defaultBindingMode: BindingMode.TwoWay); + + public DateTime Time + { + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); + } + + public static readonly StyledProperty ShowHourTicksProperty = + ClockTicks.ShowHourTicksProperty.AddOwner(); + + public bool ShowHourTicks + { + get => GetValue(ShowHourTicksProperty); + set => SetValue(ShowHourTicksProperty, value); + } + + public static readonly StyledProperty ShowMinuteTicksProperty = + ClockTicks.ShowMinuteTicksProperty.AddOwner(); + + public bool ShowMinuteTicks + { + get => GetValue(ShowMinuteTicksProperty); + set => SetValue(ShowMinuteTicksProperty, value); + } + + public static readonly StyledProperty HandBrushProperty = AvaloniaProperty.Register( + nameof(HandBrush)); + + public IBrush? HandBrush + { + get => GetValue(HandBrushProperty); + set => SetValue(HandBrushProperty, value); + } + + public static readonly StyledProperty ShowHourHandProperty = AvaloniaProperty.Register( + nameof(ShowHourHand), defaultValue: true); + + public bool ShowHourHand + { + get => GetValue(ShowHourHandProperty); + set => SetValue(ShowHourHandProperty, value); + } + + public static readonly StyledProperty ShowMinuteHandProperty = AvaloniaProperty.Register( + nameof(ShowMinuteHand), defaultValue: true); + + public bool ShowMinuteHand + { + get => GetValue(ShowMinuteHandProperty); + set => SetValue(ShowMinuteHandProperty, value); + } + + public static readonly StyledProperty ShowSecondHandProperty = AvaloniaProperty.Register( + nameof(ShowSecondHand), defaultValue: true); + + public bool ShowSecondHand + { + get => GetValue(ShowSecondHandProperty); + set => SetValue(ShowSecondHandProperty, value); + } + + + + public static readonly DirectProperty HourAngleProperty = AvaloniaProperty.RegisterDirect( + nameof(HourAngle), o => o.HourAngle); + private double _hourAngle; + public double HourAngle + { + get => _hourAngle; + private set => SetAndRaise(HourAngleProperty, ref _hourAngle, value); + } + + public static readonly DirectProperty MinuteAngleProperty = AvaloniaProperty.RegisterDirect( + nameof(MinuteAngle), o => o.MinuteAngle); + private double _minuteAngle; + public double MinuteAngle + { + get => _minuteAngle; + private set => SetAndRaise(MinuteAngleProperty, ref _minuteAngle, value); + } + + public static readonly DirectProperty SecondAngleProperty = AvaloniaProperty.RegisterDirect( + nameof(SecondAngle), o => o.SecondAngle); + + private double _secondAngle; + public double SecondAngle + { + get => _secondAngle; + private set => SetAndRaise(SecondAngleProperty, ref _secondAngle, value); + } + + static Clock() + { + TimeProperty.Changed.AddClassHandler((clock, args)=>clock.OnTimeChanged(args)); + } + + private void OnTimeChanged(AvaloniaPropertyChangedEventArgs args) + { + DateTime time = args.NewValue.Value; + var hour = time.Hour; + var minute = time.Minute; + var second = time.Second; + var hourAngle = 360.0 / 12 * hour + 360.0 / 12 / 60 * minute; + var minuteAngle = 360.0 / 60 * minute + 360.0 / 60 / 60 * second; + var secondAngle = 360.0 / 60 * second; + HourAngle = hourAngle; + MinuteAngle = minuteAngle; + SecondAngle = secondAngle; + } + + protected override Size MeasureOverride(Size availableSize) + { + double min = Math.Min(availableSize.Height, availableSize.Width); + var newSize = new Size(min, min); + var size = base.MeasureOverride(newSize); + return size; + } + + protected override Size ArrangeOverride(Size finalSize) + { + double min = Math.Min(finalSize.Height, finalSize.Width); + var newSize = new Size(min, min); + var size = base.ArrangeOverride(newSize); + return size; + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Clock/ClockTicks.cs b/src/Ursa/Controls/Clock/ClockTicks.cs new file mode 100644 index 0000000..eab1455 --- /dev/null +++ b/src/Ursa/Controls/Clock/ClockTicks.cs @@ -0,0 +1,139 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace Ursa.Controls; + +public class ClockTicks: Control +{ + private Matrix _hourRotationMatrix = Matrix.CreateRotation(Math.PI / 6); + private Matrix _minuteRotationMatrix = Matrix.CreateRotation(Math.PI / 30); + + public static readonly StyledProperty ShowHourTicksProperty = AvaloniaProperty.Register( + nameof(ShowHourTicks), true); + + public bool ShowHourTicks + { + get => GetValue(ShowHourTicksProperty); + set => SetValue(ShowHourTicksProperty, value); + } + + public static readonly StyledProperty ShowMinuteTicksProperty = AvaloniaProperty.Register( + nameof(ShowMinuteTicks), true); + + public bool ShowMinuteTicks + { + get => GetValue(ShowMinuteTicksProperty); + set => SetValue(ShowMinuteTicksProperty, value); + } + + public static readonly StyledProperty HourTickForegroundProperty = AvaloniaProperty.Register( + nameof(HourTickForeground)); + + public IBrush? HourTickForeground + { + get => GetValue(HourTickForegroundProperty); + set => SetValue(HourTickForegroundProperty, value); + } + + public static readonly StyledProperty MinuteTickForegroundProperty = AvaloniaProperty.Register( + nameof(MinuteTickForeground)); + + public IBrush? MinuteTickForeground + { + get => GetValue(MinuteTickForegroundProperty); + set => SetValue(MinuteTickForegroundProperty, value); + } + + public static readonly StyledProperty HourTickLengthProperty = AvaloniaProperty.Register( + nameof(HourTickLength), 10); + + public double HourTickLength + { + get => GetValue(HourTickLengthProperty); + set => SetValue(HourTickLengthProperty, value); + } + + public static readonly StyledProperty MinuteTickLengthProperty = AvaloniaProperty.Register( + nameof(MinuteTickLength), 5); + + public double MinuteTickLength + { + get => GetValue(MinuteTickLengthProperty); + set => SetValue(MinuteTickLengthProperty, value); + } + + public static readonly StyledProperty HourTickWidthProperty = AvaloniaProperty.Register( + nameof(HourTickWidth), 2); + + public double HourTickWidth + { + get => GetValue(HourTickWidthProperty); + set => SetValue(HourTickWidthProperty, value); + } + + public static readonly StyledProperty MinuteTickWidthProperty = AvaloniaProperty.Register( + nameof(MinuteTickWidth), 1); + + public double MinuteTickWidth + { + get => GetValue(MinuteTickWidthProperty); + set => SetValue(MinuteTickWidthProperty, value); + } + + static ClockTicks() + { + AffectsRender(ShowHourTicksProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + double minSize= Math.Min(availableSize.Width, availableSize.Height); + return new Size(minSize, minSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var minSize = Math.Min(finalSize.Width, finalSize.Height); + return new Size(minSize, minSize); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + var size = Math.Min(Bounds.Width, Bounds.Height); + var center = size / 2; + IPen hourTickPen = new Pen(HourTickForeground, HourTickWidth); + IPen minuteTickPen = new Pen(MinuteTickForeground, MinuteTickWidth); + double hourTickLength = Math.Min(center, HourTickLength); + double minuteTickLength = Math.Min(center, MinuteTickLength); + context.PushTransform(Matrix.CreateTranslation(center, center)); + if (ShowHourTicks) + { + for (int i = 0; i < 12; i++) + { + DrawTick(context, hourTickPen, center, hourTickLength); + context.PushTransform(_hourRotationMatrix); + } + } + + if (ShowMinuteTicks) + { + for (int i = 0; i < 60; i++) + { + if (i % 5 != 0) + { + DrawTick(context, minuteTickPen, center, minuteTickLength); + } + context.PushTransform(_minuteRotationMatrix); + } + } + } + + private void DrawTick(DrawingContext context, IPen pen, double center, double length) + { + var start = new Point(0, -center); + var end = new Point(0, length-center); + context.DrawLine(pen, start, end); + } +} \ No newline at end of file