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