18
demo/Ursa.Demo/Pages/ClockDemo.axaml
Normal file
18
demo/Ursa.Demo/Pages/ClockDemo.axaml
Normal file
@@ -0,0 +1,18 @@
|
||||
<UserControl
|
||||
x:Class="Ursa.Demo.Pages.ClockDemo"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels"
|
||||
x:DataType="vm:ClockDemoViewModel"
|
||||
x:CompileBindings="True"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<u:Clock HorizontalAlignment="Left" Time="{Binding Time}"></u:Clock>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
demo/Ursa.Demo/Pages/ClockDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/ClockDemo.axaml.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
31
demo/Ursa.Demo/ViewModels/ClockDemoViewModel.cs
Normal file
31
demo/Ursa.Demo/ViewModels/ClockDemoViewModel.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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";
|
||||
|
||||
61
src/Ursa.Themes.Semi/Controls/Clock.axaml
Normal file
61
src/Ursa.Themes.Semi/Controls/Clock.axaml
Normal file
@@ -0,0 +1,61 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converter="clr-namespace:Ursa.Themes.Semi.Converters"
|
||||
xmlns:u="https://irihi.tech/ursa">
|
||||
<!-- Add Resources Here -->
|
||||
<ControlTheme x:Key="{x:Type u:Clock}" TargetType="u:Clock">
|
||||
<Setter Property="HandBrush" Value="{DynamicResource SemiGrey6}"/>
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:Clock">
|
||||
<Grid>
|
||||
<u:ClockTicks
|
||||
ShowHourTicks="{TemplateBinding ShowHourTicks}"
|
||||
ShowMinuteTicks="{TemplateBinding ShowMinuteTicks}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
HourTickForeground="{DynamicResource SemiGrey6}"
|
||||
MinuteTickForeground="{DynamicResource SemiGrey4}" />
|
||||
<UniformGrid Rows="2" IsVisible="{TemplateBinding ShowHourHand}">
|
||||
<Border
|
||||
Width="16"
|
||||
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Bounds.Width, Converter={x:Static converter:ClockHandLengthConverter.Hour}}"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{TemplateBinding HandBrush}"
|
||||
CornerRadius="8" />
|
||||
<UniformGrid.RenderTransform>
|
||||
<RotateTransform Angle="{Binding HourAngle, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</UniformGrid.RenderTransform>
|
||||
</UniformGrid>
|
||||
<UniformGrid Rows="2" IsVisible="{TemplateBinding ShowMinuteHand}">
|
||||
<Border
|
||||
Width="8"
|
||||
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Bounds.Width, Converter={x:Static converter:ClockHandLengthConverter.Minute}}"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{TemplateBinding HandBrush}"
|
||||
CornerRadius="4" />
|
||||
<UniformGrid.RenderTransform>
|
||||
<RotateTransform Angle="{Binding MinuteAngle, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</UniformGrid.RenderTransform>
|
||||
</UniformGrid>
|
||||
<UniformGrid Rows="2" IsVisible="{TemplateBinding ShowSecondHand}">
|
||||
<Border
|
||||
Width="4"
|
||||
Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Bounds.Width, Converter={x:Static converter:ClockHandLengthConverter.Second}}"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{TemplateBinding HandBrush}"
|
||||
CornerRadius="4" />
|
||||
<UniformGrid.RenderTransform>
|
||||
<RotateTransform Angle="{Binding SecondAngle, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||
</UniformGrid.RenderTransform>
|
||||
</UniformGrid>
|
||||
<Ellipse
|
||||
Width="20"
|
||||
Height="20"
|
||||
Fill="White"
|
||||
Stroke="{DynamicResource SemiBlue5}"
|
||||
StrokeThickness="3" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -6,6 +6,7 @@
|
||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||
<ResourceInclude Source="ControlClassesInput.axaml" />
|
||||
<ResourceInclude Source="Clock.axaml" />
|
||||
<ResourceInclude Source="Dialog.axaml" />
|
||||
<ResourceInclude Source="DialogShared.axaml" />
|
||||
<ResourceInclude Source="DisableContainer.axaml" />
|
||||
|
||||
27
src/Ursa.Themes.Semi/Converters/ClockHandLengthConverter.cs
Normal file
27
src/Ursa.Themes.Semi/Converters/ClockHandLengthConverter.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
141
src/Ursa/Controls/Clock/Clock.cs
Normal file
141
src/Ursa/Controls/Clock/Clock.cs
Normal file
@@ -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<DateTime> TimeProperty = AvaloniaProperty.Register<Clock, DateTime>(
|
||||
nameof(Time), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public DateTime Time
|
||||
{
|
||||
get => GetValue(TimeProperty);
|
||||
set => SetValue(TimeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowHourTicksProperty =
|
||||
ClockTicks.ShowHourTicksProperty.AddOwner<Clock>();
|
||||
|
||||
public bool ShowHourTicks
|
||||
{
|
||||
get => GetValue(ShowHourTicksProperty);
|
||||
set => SetValue(ShowHourTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteTicksProperty =
|
||||
ClockTicks.ShowMinuteTicksProperty.AddOwner<Clock>();
|
||||
|
||||
public bool ShowMinuteTicks
|
||||
{
|
||||
get => GetValue(ShowMinuteTicksProperty);
|
||||
set => SetValue(ShowMinuteTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> HandBrushProperty = AvaloniaProperty.Register<Clock, IBrush?>(
|
||||
nameof(HandBrush));
|
||||
|
||||
public IBrush? HandBrush
|
||||
{
|
||||
get => GetValue(HandBrushProperty);
|
||||
set => SetValue(HandBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowHourHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowHourHand), defaultValue: true);
|
||||
|
||||
public bool ShowHourHand
|
||||
{
|
||||
get => GetValue(ShowHourHandProperty);
|
||||
set => SetValue(ShowHourHandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowMinuteHand), defaultValue: true);
|
||||
|
||||
public bool ShowMinuteHand
|
||||
{
|
||||
get => GetValue(ShowMinuteHandProperty);
|
||||
set => SetValue(ShowMinuteHandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowSecondHandProperty = AvaloniaProperty.Register<Clock, bool>(
|
||||
nameof(ShowSecondHand), defaultValue: true);
|
||||
|
||||
public bool ShowSecondHand
|
||||
{
|
||||
get => GetValue(ShowSecondHandProperty);
|
||||
set => SetValue(ShowSecondHandProperty, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static readonly DirectProperty<Clock, double> HourAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
nameof(HourAngle), o => o.HourAngle);
|
||||
private double _hourAngle;
|
||||
public double HourAngle
|
||||
{
|
||||
get => _hourAngle;
|
||||
private set => SetAndRaise(HourAngleProperty, ref _hourAngle, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<Clock, double> MinuteAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
nameof(MinuteAngle), o => o.MinuteAngle);
|
||||
private double _minuteAngle;
|
||||
public double MinuteAngle
|
||||
{
|
||||
get => _minuteAngle;
|
||||
private set => SetAndRaise(MinuteAngleProperty, ref _minuteAngle, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<Clock, double> SecondAngleProperty = AvaloniaProperty.RegisterDirect<Clock, double>(
|
||||
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, DateTime>((clock, args)=>clock.OnTimeChanged(args));
|
||||
}
|
||||
|
||||
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<DateTime> 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;
|
||||
}
|
||||
}
|
||||
139
src/Ursa/Controls/Clock/ClockTicks.cs
Normal file
139
src/Ursa/Controls/Clock/ClockTicks.cs
Normal file
@@ -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<bool> ShowHourTicksProperty = AvaloniaProperty.Register<ClockTicks, bool>(
|
||||
nameof(ShowHourTicks), true);
|
||||
|
||||
public bool ShowHourTicks
|
||||
{
|
||||
get => GetValue(ShowHourTicksProperty);
|
||||
set => SetValue(ShowHourTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> ShowMinuteTicksProperty = AvaloniaProperty.Register<ClockTicks, bool>(
|
||||
nameof(ShowMinuteTicks), true);
|
||||
|
||||
public bool ShowMinuteTicks
|
||||
{
|
||||
get => GetValue(ShowMinuteTicksProperty);
|
||||
set => SetValue(ShowMinuteTicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> HourTickForegroundProperty = AvaloniaProperty.Register<ClockTicks, IBrush?>(
|
||||
nameof(HourTickForeground));
|
||||
|
||||
public IBrush? HourTickForeground
|
||||
{
|
||||
get => GetValue(HourTickForegroundProperty);
|
||||
set => SetValue(HourTickForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> MinuteTickForegroundProperty = AvaloniaProperty.Register<ClockTicks, IBrush?>(
|
||||
nameof(MinuteTickForeground));
|
||||
|
||||
public IBrush? MinuteTickForeground
|
||||
{
|
||||
get => GetValue(MinuteTickForegroundProperty);
|
||||
set => SetValue(MinuteTickForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> HourTickLengthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(HourTickLength), 10);
|
||||
|
||||
public double HourTickLength
|
||||
{
|
||||
get => GetValue(HourTickLengthProperty);
|
||||
set => SetValue(HourTickLengthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MinuteTickLengthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(MinuteTickLength), 5);
|
||||
|
||||
public double MinuteTickLength
|
||||
{
|
||||
get => GetValue(MinuteTickLengthProperty);
|
||||
set => SetValue(MinuteTickLengthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> HourTickWidthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(HourTickWidth), 2);
|
||||
|
||||
public double HourTickWidth
|
||||
{
|
||||
get => GetValue(HourTickWidthProperty);
|
||||
set => SetValue(HourTickWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MinuteTickWidthProperty = AvaloniaProperty.Register<ClockTicks, double>(
|
||||
nameof(MinuteTickWidth), 1);
|
||||
|
||||
public double MinuteTickWidth
|
||||
{
|
||||
get => GetValue(MinuteTickWidthProperty);
|
||||
set => SetValue(MinuteTickWidthProperty, value);
|
||||
}
|
||||
|
||||
static ClockTicks()
|
||||
{
|
||||
AffectsRender<ClockTicks>(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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user