diff --git a/demo/Ursa.Demo/Pages/TimelineDemo.axaml b/demo/Ursa.Demo/Pages/TimelineDemo.axaml new file mode 100644 index 0000000..b217fad --- /dev/null +++ b/demo/Ursa.Demo/Pages/TimelineDemo.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs b/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs new file mode 100644 index 0000000..fe07b76 --- /dev/null +++ b/demo/Ursa.Demo/Pages/TimelineDemo.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.Demo.ViewModels; + +namespace Ursa.Demo.Pages; + +public partial class TimelineDemo : UserControl +{ + public TimelineDemo() + { + InitializeComponent(); + this.DataContext = new TimelineDemoViewModel(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Ursa.Demo.csproj b/demo/Ursa.Demo/Ursa.Demo.csproj index 7cc1b6c..f66e639 100644 --- a/demo/Ursa.Demo/Ursa.Demo.csproj +++ b/demo/Ursa.Demo/Ursa.Demo.csproj @@ -5,6 +5,7 @@ enable true app.manifest + false diff --git a/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs new file mode 100644 index 0000000..2420ee3 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs @@ -0,0 +1,115 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Ursa.Controls; + +namespace Ursa.Demo.ViewModels; + +public class TimelineDemoViewModel: ObservableObject +{ + public TimelineItemViewModel[] Items { get; } = + { + new() + { + Time = DateTime.Now, + TimeFormat = "yyyy-MM-dd HH:mm:ss", + Description = "Item 1", + Content = "First", + ItemType = TimelineItemType.Success, + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 2", + Content = "Content 2", + ItemType = TimelineItemType.Success, + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 3", + Content = "Content 3", + ItemType = TimelineItemType.Ongoing, + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 4", + Content = "Content 4" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 5", + Content = "Content 5" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 6", + Content = "Content 6" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 7", + Content = "Content 71231" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 8", + Content = "Content 8123123" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 9", + Content = "Content 9123123" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 10", + Content = "Content 1231231231231231231230" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 11", + Content = "Content 11231231" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 12", + Content = "Content 12123123123123" + }, + new() + { + Time = DateTime.Now, + TimeFormat = "HH:mm:ss", + Description = "Item 13", + Content = "Last" + } + }; +} + +public class TimelineItemViewModel: ObservableObject +{ + public DateTime Time { get; set; } + public string? TimeFormat { get; set; } + public string? Description { get; set; } + public string? Content { get; set; } + public TimelineItemType ItemType { get; set; } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Views/MainWindow.axaml b/demo/Ursa.Demo/Views/MainWindow.axaml index de1f2dd..9ba93b8 100644 --- a/demo/Ursa.Demo/Views/MainWindow.axaml +++ b/demo/Ursa.Demo/Views/MainWindow.axaml @@ -35,6 +35,9 @@ + + + diff --git a/src/Ursa.Themes.Semi/Controls/Timeline.axaml b/src/Ursa.Themes.Semi/Controls/Timeline.axaml new file mode 100644 index 0000000..63e311c --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/Timeline.axaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 4d15016..7c08f47 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -5,5 +5,6 @@ + diff --git a/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs b/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs new file mode 100644 index 0000000..2b2d1ab --- /dev/null +++ b/src/Ursa.Themes.Semi/Converters/TimelineItemTypeToIconForegroundConverter.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Ursa.Controls; + +namespace Ursa.Themes.Semi.Converters; + +public class TimelineItemTypeToIconForegroundConverter: AvaloniaObject, IMultiValueConverter +{ + public static readonly StyledProperty DefaultBrushProperty = AvaloniaProperty.Register( + nameof(DefaultBrush)); + + public IBrush DefaultBrush + { + get => GetValue(DefaultBrushProperty); + set => SetValue(DefaultBrushProperty, value); + } + + public static readonly StyledProperty OngoingBrushProperty = AvaloniaProperty.Register( + nameof(OngoingBrush)); + + public IBrush OngoingBrush + { + get => GetValue(OngoingBrushProperty); + set => SetValue(OngoingBrushProperty, value); + } + + public static readonly StyledProperty SuccessBrushProperty = AvaloniaProperty.Register( + nameof(SuccessBrush)); + + public IBrush SuccessBrush + { + get => GetValue(SuccessBrushProperty); + set => SetValue(SuccessBrushProperty, value); + } + + public static readonly StyledProperty WarningBrushProperty = AvaloniaProperty.Register( + nameof(WarningBrush)); + + public IBrush WarningBrush + { + get => GetValue(WarningBrushProperty); + set => SetValue(WarningBrushProperty, value); + } + + public static readonly StyledProperty ErrorBrushProperty = AvaloniaProperty.Register( + nameof(ErrorBrush)); + + public IBrush ErrorBrush + { + get => GetValue(ErrorBrushProperty); + set => SetValue(ErrorBrushProperty, value); + } + + + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values[0] is TimelineItemType type) + { + switch (type) + { + case TimelineItemType.Error: + return ErrorBrush; + case TimelineItemType.Warning: + return WarningBrush; + case TimelineItemType.Success: + return SuccessBrush; + case TimelineItemType.Ongoing: + return OngoingBrush; + case TimelineItemType.Default: + if (values[1] is IBrush brush) + { + return brush; + } + return DefaultBrush; + } + } + return AvaloniaProperty.UnsetValue; + } +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Light/Timeline.axaml b/src/Ursa.Themes.Semi/Themes/Light/Timeline.axaml new file mode 100644 index 0000000..e879046 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Light/Timeline.axaml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml index 7a530cc..004d6a1 100644 --- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml @@ -5,5 +5,6 @@ + diff --git a/src/Ursa/Controls/Timeline/Timeline.cs b/src/Ursa/Controls/Timeline/Timeline.cs new file mode 100644 index 0000000..3304ab0 --- /dev/null +++ b/src/Ursa/Controls/Timeline/Timeline.cs @@ -0,0 +1,56 @@ +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; + +namespace Ursa.Controls; + +public class Timeline: ItemsControl +{ + + public static readonly StyledProperty ItemDescriptionTemplateProperty = AvaloniaProperty.Register( + nameof(ItemDescriptionTemplate)); + + public IDataTemplate? ItemDescriptionTemplate + { + get => GetValue(ItemDescriptionTemplateProperty); + set => SetValue(ItemDescriptionTemplateProperty, value); + } + + public Timeline() + { + ItemsView.CollectionChanged+=ItemsViewOnCollectionChanged; + } + + private void ItemsViewOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + RefreshTimelineItems(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + RefreshTimelineItems(); + } + + private void RefreshTimelineItems() + { + for (int i = 0; i < this.LogicalChildren.Count; i++) + { + if (this.LogicalChildren[i] is TimelineItem t) + { + t.SetIndex(i == 0, i == this.LogicalChildren.Count - 1); + } + else if (this.LogicalChildren[i] is ContentPresenter { Child: TimelineItem t2 }) + { + t2.SetIndex(i == 0, i == this.LogicalChildren.Count - 1); + } + } + } + +} \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelineFormatConverter.cs b/src/Ursa/Controls/Timeline/TimelineFormatConverter.cs new file mode 100644 index 0000000..48cdc8f --- /dev/null +++ b/src/Ursa/Controls/Timeline/TimelineFormatConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Controls; + +public class TimelineFormatConverter: IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count> 1 && values[0] is DateTime date && values[1] is string s) + { + return date.ToString(s, culture); + } + return AvaloniaProperty.UnsetValue; + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelineItem.cs b/src/Ursa/Controls/Timeline/TimelineItem.cs new file mode 100644 index 0000000..9ba9f9c --- /dev/null +++ b/src/Ursa/Controls/Timeline/TimelineItem.cs @@ -0,0 +1,94 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Media; + +namespace Ursa.Controls; + +[PseudoClasses(PC_First, PC_Last, PC_Default, PC_Ongoing, PC_Success, PC_Warning, PC_Error)] +public class TimelineItem: ContentControl +{ + private const string PC_First = ":first"; + private const string PC_Last = ":last"; + private const string PC_Default = ":default"; + private const string PC_Ongoing = ":ongoing"; + private const string PC_Success = ":success"; + private const string PC_Warning = ":warning"; + private const string PC_Error = ":error"; + + private static readonly IReadOnlyDictionary _itemTypeMapping = new Dictionary + { + {TimelineItemType.Default, PC_Default}, + {TimelineItemType.Ongoing, PC_Ongoing}, + {TimelineItemType.Success, PC_Success}, + {TimelineItemType.Warning, PC_Warning}, + {TimelineItemType.Error, PC_Error}, + }; + + public static readonly StyledProperty IconForegroundProperty = + AvaloniaProperty.Register(nameof(IconForeground)); + + public IBrush IconForeground + { + get => GetValue(IconForegroundProperty); + set => SetValue(IconForegroundProperty, value); + } + + public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( + nameof(Time)); + public DateTime Time + { + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); + } + + public static readonly StyledProperty TimeFormatProperty = AvaloniaProperty.Register( + nameof(TimeFormat), defaultValue:CultureInfo.CurrentUICulture.DateTimeFormat.ShortDatePattern); + + public string? TimeFormat + { + get => GetValue(TimeFormatProperty); + set => SetValue(TimeFormatProperty, value); + } + + public static readonly StyledProperty DescriptionTemplateProperty = AvaloniaProperty.Register( + nameof(DescriptionTemplate)); + + public IDataTemplate DescriptionTemplate + { + get => GetValue(DescriptionTemplateProperty); + set => SetValue(DescriptionTemplateProperty, value); + } + + public static readonly StyledProperty ItemTypeProperty = AvaloniaProperty.Register( + nameof(ItemType)); + + public TimelineItemType ItemType + { + get => GetValue(ItemTypeProperty); + set => SetValue(ItemTypeProperty, value); + } + + internal void SetIndex(bool isFirst, bool isLast) + { + PseudoClasses.Set(PC_First, isFirst); + PseudoClasses.Set(PC_Last, isLast); + } + + static TimelineItem() + { + ItemTypeProperty.Changed.AddClassHandler((o, e) => { o.OnItemTypeChanged(e); }); + } + + private void OnItemTypeChanged(AvaloniaPropertyChangedEventArgs args) + { + var oldValue = args.GetOldValue(); + var newValue = args.GetNewValue(); + PseudoClasses.Set(_itemTypeMapping[oldValue], false); + PseudoClasses.Set(_itemTypeMapping[newValue], true); + } + +} \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelineItemType.cs b/src/Ursa/Controls/Timeline/TimelineItemType.cs new file mode 100644 index 0000000..6b90703 --- /dev/null +++ b/src/Ursa/Controls/Timeline/TimelineItemType.cs @@ -0,0 +1,10 @@ +namespace Ursa.Controls; + +public enum TimelineItemType +{ + Default, + Ongoing, + Success, + Warning, + Error, +} \ No newline at end of file