diff --git a/demo/Ursa.Demo.Desktop/Ursa.Demo.Desktop.csproj b/demo/Ursa.Demo.Desktop/Ursa.Demo.Desktop.csproj index 3d3f388..64e44ce 100644 --- a/demo/Ursa.Demo.Desktop/Ursa.Demo.Desktop.csproj +++ b/demo/Ursa.Demo.Desktop/Ursa.Demo.Desktop.csproj @@ -6,6 +6,7 @@ net7.0 enable true + false diff --git a/demo/Ursa.Demo/Converters/TimelineIconConverter.cs b/demo/Ursa.Demo/Converters/TimelineIconConverter.cs new file mode 100644 index 0000000..04d46f4 --- /dev/null +++ b/demo/Ursa.Demo/Converters/TimelineIconConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Ursa.Controls; + +namespace Ursa.Demo.Converters; + +public class TimelineIconConverter: IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is TimelineItemType t) + { + return t switch + { + TimelineItemType.Success => Brushes.Green, + TimelineItemType.Ongoing => Brushes.Blue, + TimelineItemType.Error => Brushes.Red, + _ => Brushes.Gray + }; + } + return AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/IntroductionDemo.axaml b/demo/Ursa.Demo/Pages/IntroductionDemo.axaml index 560ab49..f06ee98 100644 --- a/demo/Ursa.Demo/Pages/IntroductionDemo.axaml +++ b/demo/Ursa.Demo/Pages/IntroductionDemo.axaml @@ -144,27 +144,29 @@ IPAddress="{Binding Address}" /> - + + Content="Step 1" + Header="ToDo" + Type="Default" + Time="2023-01-14 09:24:05"/> + Content="Step 2" + Header="Start" + Position="Right" + Type="Ongoing" + Time="2024-01-04 22:32:58"/> + Content="Step 3" + Header="In Between" + Type="Warning" + Time="2024-01-05 00:08:29"/> - + Content="Step 4" + Header="Finished" + Position="Right" + Type="Success" + Time="2024-01-05 00:27:44"/> diff --git a/demo/Ursa.Demo/Pages/TimelineDemo.axaml b/demo/Ursa.Demo/Pages/TimelineDemo.axaml index b217fad..262ac3e 100644 --- a/demo/Ursa.Demo/Pages/TimelineDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimelineDemo.axaml @@ -4,55 +4,84 @@ 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:selectors="clr-namespace:Ursa.Demo.TemplateSelectors" xmlns:u="https://irihi.tech/ursa" xmlns:viewModels="clr-namespace:Ursa.Demo.ViewModels" d:DesignHeight="450" d:DesignWidth="800" - x:CompileBindings="False" + x:CompileBindings="True" x:DataType="viewModels:TimelineDemoViewModel" mc:Ignorable="d"> - + + + + + + + + + - - - - - - + + + - - - - - - - - - - - - + - + + + + + + + + + + diff --git a/demo/Ursa.Demo/TemplateSelectors/TimelineIconTemplateSelector.cs b/demo/Ursa.Demo/TemplateSelectors/TimelineIconTemplateSelector.cs new file mode 100644 index 0000000..a278e6f --- /dev/null +++ b/demo/Ursa.Demo/TemplateSelectors/TimelineIconTemplateSelector.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Media; +using Ursa.Controls; + +namespace Ursa.Demo.TemplateSelectors; + +public class TimelineIconTemplateSelector: ResourceDictionary, IDataTemplate +{ + + public Control? Build(object? param) + { + if (param is TimelineItemType t) + { + string s = t.ToString(); + if (ContainsKey(s)) + { + object? o = this[s]; + if (o is SolidColorBrush c) + { + var ellipse = new Ellipse() { Width = 12, Height = 12, Fill = c }; + return ellipse; + } + } + } + return null; + } + + public bool Match(object? data) + { + return data is TimelineItemType; + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs index fc9e819..54cb092 100644 --- a/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/TimelineDemoViewModel.cs @@ -11,41 +11,24 @@ public class TimelineDemoViewModel: ViewModelBase new() { Time = DateTime.Now, - TimeFormat = "yyyy-MM-dd HH:mm:ss", Description = "Item 1", - Content = "First", + Header = "审核中", 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", + Header = "发布成功", 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" - }, + Description = "Item 3", + Header = "审核失败", + ItemType = TimelineItemType.Error, + } }; } @@ -54,6 +37,6 @@ 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 string? Header { get; set; } public TimelineItemType ItemType { get; set; } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Timeline.axaml b/src/Ursa.Themes.Semi/Controls/Timeline.axaml index b8f62fa..913e62c 100644 --- a/src/Ursa.Themes.Semi/Controls/Timeline.axaml +++ b/src/Ursa.Themes.Semi/Controls/Timeline.axaml @@ -6,9 +6,9 @@ - - - + + + @@ -26,104 +26,172 @@ + WarningBrush="{DynamicResource WarningTimelineIconForeground}" /> + + - + + - - - + + + VerticalAlignment="Center" + Content="{TemplateBinding Icon}" + ContentTemplate="{TemplateBinding IconTemplate}" /> + - - + Grid.Column="2" + Margin="8,4" + VerticalAlignment="Top" + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}" + FontSize="14" + Foreground="{DynamicResource SemiGrey9}" /> + + + - - - + + - - - - - - - + + + + + + + + + + + diff --git a/src/Ursa/Controls/Timeline/Timeline.cs b/src/Ursa/Controls/Timeline/Timeline.cs index 3304ab0..a318a58 100644 --- a/src/Ursa/Controls/Timeline/Timeline.cs +++ b/src/Ursa/Controls/Timeline/Timeline.cs @@ -1,56 +1,220 @@ 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; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Metadata; namespace Ursa.Controls; public class Timeline: ItemsControl { + private static readonly FuncTemplate DefaultPanel = new((Func)(() => new TimelinePanel())); - public static readonly StyledProperty ItemDescriptionTemplateProperty = AvaloniaProperty.Register( - nameof(ItemDescriptionTemplate)); + public static readonly StyledProperty IconMemberBindingProperty = AvaloniaProperty.Register( + nameof(IconMemberBinding)); - public IDataTemplate? ItemDescriptionTemplate + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? IconMemberBinding { - get => GetValue(ItemDescriptionTemplateProperty); - set => SetValue(ItemDescriptionTemplateProperty, value); + get => GetValue(IconMemberBindingProperty); + set => SetValue(IconMemberBindingProperty, value); } - public Timeline() + public static readonly StyledProperty HeaderMemberBindingProperty = AvaloniaProperty.Register( + nameof(HeaderMemberBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? HeaderMemberBinding { - ItemsView.CollectionChanged+=ItemsViewOnCollectionChanged; + get => GetValue(HeaderMemberBindingProperty); + set => SetValue(HeaderMemberBindingProperty, value); } - private void ItemsViewOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + public static readonly StyledProperty ContentMemberBindingProperty = AvaloniaProperty.Register( + nameof(ContentMemberBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? ContentMemberBinding { - RefreshTimelineItems(); + get => GetValue(ContentMemberBindingProperty); + set => SetValue(ContentMemberBindingProperty, value); + } + + + public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( + nameof(IconTemplate)); + + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + public static readonly StyledProperty DescriptionTemplateProperty = AvaloniaProperty.Register( + nameof(DescriptionTemplate)); + + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IDataTemplate? DescriptionTemplate { - base.OnPropertyChanged(change); - RefreshTimelineItems(); + get => GetValue(DescriptionTemplateProperty); + set => SetValue(DescriptionTemplateProperty, value); } - private void RefreshTimelineItems() + public static readonly StyledProperty TimeMemberBindingProperty = AvaloniaProperty.Register( + nameof(TimeMemberBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? TimeMemberBinding { - for (int i = 0; i < this.LogicalChildren.Count; i++) + get => GetValue(TimeMemberBindingProperty); + set => SetValue(TimeMemberBindingProperty, value); + } + + public static readonly StyledProperty TimeFormatProperty = AvaloniaProperty.Register( + nameof(TimeFormat), defaultValue:"yyyy-MM-dd HH:mm:ss"); + + public string? TimeFormat + { + get => GetValue(TimeFormatProperty); + set => SetValue(TimeFormatProperty, value); + } + + + public static readonly StyledProperty ModeProperty = AvaloniaProperty.Register( + nameof(Mode)); + + public TimelineDisplayMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + static Timeline() + { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + ModeProperty.Changed.AddClassHandler((t, e) => { t.OnDisplayModeChanged(e); }); + } + + private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) + { + if (this.ItemsPanelRoot is TimelinePanel panel) { - if (this.LogicalChildren[i] is TimelineItem t) + panel.Mode = e.NewValue.Value; + SetItemMode(); + } + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + recycleKey = null; + return item is not TimelineItem; + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + if (item is TimelineItem t) return t; + return new TimelineItem(); + } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if (container is TimelineItem t) + { + bool start = index == 0; + bool end = index == ItemCount - 1; + t.SetEnd(start, end); + if (IconMemberBinding is not null) { - t.SetIndex(i == 0, i == this.LogicalChildren.Count - 1); + t.Bind(TimelineItem.IconProperty, IconMemberBinding); } - else if (this.LogicalChildren[i] is ContentPresenter { Child: TimelineItem t2 }) + if (HeaderMemberBinding != null) { - t2.SetIndex(i == 0, i == this.LogicalChildren.Count - 1); + t.Bind(HeaderedContentControl.HeaderProperty, HeaderMemberBinding); + } + if (ContentMemberBinding != null) + { + t.Bind(ContentControl.ContentProperty, ContentMemberBinding); + } + if (TimeMemberBinding != null) + { + t.Bind(TimelineItem.TimeProperty, TimeMemberBinding); + } + + t.SetIfUnset(TimelineItem.TimeFormatProperty, TimeFormat); + t.SetIfUnset(TimelineItem.IconTemplateProperty, IconTemplate); + t.SetIfUnset(HeaderedContentControl.HeaderTemplateProperty, ItemTemplate); + t.SetIfUnset(ContentControl.ContentTemplateProperty, DescriptionTemplate); + } + + } + + protected override Size ArrangeOverride(Size finalSize) + { + var panel = this.ItemsPanelRoot as TimelinePanel; + panel.Mode = this.Mode; + SetItemMode(); + return base.ArrangeOverride(finalSize); + } + + private void SetItemMode() + { + if (ItemsPanelRoot is TimelinePanel panel) + { + var items = panel.Children.OfType(); + if (Mode == TimelineDisplayMode.Left) + { + foreach (var item in items) + { + SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Left); + } + } + else if (Mode == TimelineDisplayMode.Right) + { + foreach (var item in items) + { + SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Right); + } + } + else if (Mode == TimelineDisplayMode.Center) + { + foreach (var item in items) + { + SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Separate); + } + } + else if (Mode == TimelineDisplayMode.Alternate) + { + bool left = false; + foreach (var item in items) + { + if (left) + { + SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Left); + } + else + { + SetIfUnset(item, TimelineItem.PositionProperty, TimelineItemPosition.Right); + } + left = !left; + } } } } + private void SetIfUnset(AvaloniaObject target, StyledProperty property, T value) + { + if (!target.IsSet(property)) + target.SetCurrentValue(property, value); + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelineDisplayMode.cs b/src/Ursa/Controls/Timeline/TimelineDisplayMode.cs new file mode 100644 index 0000000..ec01a90 --- /dev/null +++ b/src/Ursa/Controls/Timeline/TimelineDisplayMode.cs @@ -0,0 +1,22 @@ +namespace Ursa.Controls; + +public enum TimelineDisplayMode +{ + Left, + Center, + Right, + Alternate, +} + +/// +/// Placement of timeline. +/// Left means line is placed left to TimelineItem content. +/// Right means line is placed right to TimelineItem content. +/// Separate means line is placed between TimelineItem content and time. +/// +public enum TimelineItemPosition +{ + Left, + Right, + Separate, +} \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelineItem.cs b/src/Ursa/Controls/Timeline/TimelineItem.cs index 0958a31..9eecece 100644 --- a/src/Ursa/Controls/Timeline/TimelineItem.cs +++ b/src/Ursa/Controls/Timeline/TimelineItem.cs @@ -1,45 +1,106 @@ -using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; 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, PC_None)] -public class TimelineItem: ContentControl +[PseudoClasses(PC_First, PC_Last, PC_EmptyIcon, PC_AllLeft, PC_AllRight, PC_Separate)] +[TemplatePart(PART_Header, typeof(ContentPresenter))] +[TemplatePart(PART_Icon, typeof(Panel))] +[TemplatePart(PART_Content, typeof(ContentPresenter))] +[TemplatePart(PART_Time, typeof(TextBlock))] +[TemplatePart(PART_RootGrid, typeof(Grid))] +public class TimelineItem: HeaderedContentControl { - 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 const string PC_None = ":none"; + public const string PC_First = ":first"; + public const string PC_Last = ":last"; + public const string PC_EmptyIcon = ":empty-icon"; + public const string PC_AllLeft=":all-left"; + public const string PC_AllRight=":all-right"; + public const string PC_Separate = ":separate"; + public const string PART_Header = "PART_Header"; + public const string PART_Icon = "PART_Icon"; + public const string PART_Content = "PART_Content"; + public const string PART_Time = "PART_Time"; + public const string PART_RootGrid = "PART_RootGrid"; + + private ContentPresenter? _headerPresenter; + private Panel? _iconPresenter; + private ContentPresenter? _contentPresenter; + private TextBlock? _timePresenter; + private Grid? _rootGrid; + + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( + nameof(Icon)); - private static readonly IReadOnlyDictionary _itemTypeMapping = new Dictionary + public object? Icon { - {TimelineItemType.Default, PC_Default}, - {TimelineItemType.Ongoing, PC_Ongoing}, - {TimelineItemType.Success, PC_Success}, - {TimelineItemType.Warning, PC_Warning}, - {TimelineItemType.Error, PC_Error}, - }; + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } - public static readonly StyledProperty IconForegroundProperty = - AvaloniaProperty.Register(nameof(IconForeground)); + public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( + nameof(IconTemplate)); - public IBrush IconForeground + public IDataTemplate? IconTemplate { - get => GetValue(IconForegroundProperty); - set => SetValue(IconForegroundProperty, value); + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty TypeProperty = AvaloniaProperty.Register( + nameof(Type)); + + public TimelineItemType Type + { + get => GetValue(TypeProperty); + set => SetValue(TypeProperty, value); + } + + public static readonly StyledProperty PositionProperty = AvaloniaProperty.Register( + nameof(Position), defaultValue: TimelineItemPosition.Right); + + public TimelineItemPosition Position + { + get => GetValue(PositionProperty); + set => SetValue(PositionProperty, value); + } + + public static readonly DirectProperty LeftWidthProperty = AvaloniaProperty.RegisterDirect( + nameof(LeftWidth), o => o.LeftWidth, (o, v) => o.LeftWidth = v); + private double _leftWidth; + public double LeftWidth + { + get => _leftWidth; + set => SetAndRaise(LeftWidthProperty, ref _leftWidth, value); + } + + public static readonly DirectProperty IconWidthProperty = AvaloniaProperty.RegisterDirect( + nameof(IconWidth), o => o.IconWidth, (o, v) => o.IconWidth = v); + private double _iconWidth; + public double IconWidth + { + get => _iconWidth; + set => SetAndRaise(IconWidthProperty, ref _iconWidth, value); + } + + public static readonly DirectProperty RightWidthProperty = AvaloniaProperty.RegisterDirect( + nameof(RightWidth), o => o.RightWidth, (o, v) => o.RightWidth = v); + private double _rightWidth; + public double RightWidth + { + get => _rightWidth; + set => SetAndRaise(RightWidthProperty, ref _rightWidth, value); } public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( nameof(Time)); + public DateTime Time { get => GetValue(TimeProperty); @@ -47,7 +108,7 @@ public class TimelineItem: ContentControl } public static readonly StyledProperty TimeFormatProperty = AvaloniaProperty.Register( - nameof(TimeFormat), defaultValue:CultureInfo.CurrentUICulture.DateTimeFormat.ShortDatePattern); + nameof(TimeFormat)); public string? TimeFormat { @@ -55,47 +116,86 @@ public class TimelineItem: ContentControl 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); }); - IconForegroundProperty.Changed.AddClassHandler((o, e) => { o.OnIconForegroundChanged(e); }); + IconProperty.Changed.AddClassHandler((item, args) => { item.OnIconChanged(args); }); + PositionProperty.Changed.AddClassHandler((item, args) => { item.OnModeChanged(args); }); + AffectsMeasure(LeftWidthProperty, RightWidthProperty, IconWidthProperty); } - private void OnItemTypeChanged(AvaloniaPropertyChangedEventArgs args) + private void OnModeChanged(AvaloniaPropertyChangedEventArgs args) { - var oldValue = args.GetOldValue(); - var newValue = args.GetNewValue(); - PseudoClasses.Set(_itemTypeMapping[oldValue], false); - PseudoClasses.Set(_itemTypeMapping[newValue], true); + SetMode(args.NewValue.Value); } - private void OnIconForegroundChanged(AvaloniaPropertyChangedEventArgs args) + private void SetMode(TimelineItemPosition mode) { - IBrush? newValue = args.GetOldValue(); - PseudoClasses.Set(PC_None, newValue is null); + PseudoClasses.Set(PC_AllLeft, mode == TimelineItemPosition.Left); + PseudoClasses.Set(PC_AllRight, mode == TimelineItemPosition.Right); + PseudoClasses.Set(PC_Separate, mode == TimelineItemPosition.Separate); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _rootGrid = e.NameScope.Find(PART_RootGrid); + _headerPresenter = e.NameScope.Find(PART_Header); + _iconPresenter = e.NameScope.Find(PART_Icon); + _contentPresenter = e.NameScope.Find(PART_Content); + _timePresenter = e.NameScope.Find(PART_Time); + PseudoClasses.Set(PC_EmptyIcon, Icon is null); + SetMode(Position); + } + + private void OnIconChanged(AvaloniaPropertyChangedEventArgs args) + { + PseudoClasses.Set(PC_EmptyIcon, args.NewValue.Value is null); + } + + internal void SetEnd(bool start, bool end) + { + PseudoClasses.Set(PC_First, start); + PseudoClasses.Set(PC_Last, end); + } + + internal (double left, double mid, double right) GetWidth() + { + if (_headerPresenter is null) return new ValueTuple(0, 0, 0); + double header = _headerPresenter?.DesiredSize.Width ?? 0; + double icon = _iconPresenter?.DesiredSize.Width ?? 0; + double content = _contentPresenter?.DesiredSize.Width ?? 0; + double time = _timePresenter?.DesiredSize.Width ?? 0; + double max = Math.Max(header, content); + if (Position == TimelineItemPosition.Left) + { + max = Math.Max(max, time); + return (0, icon, max); + } + if (Position == TimelineItemPosition.Right) + { + max = Math.Max(max, time); + return (max , icon, 0); + } + if (Position == TimelineItemPosition.Separate) + { + return (time, icon, max); + } + return new ValueTuple(0, 0, 0); + } + + internal void SetWidth(double? left, double? mid, double? right) + { + if (_rootGrid is null) return; + _rootGrid.ColumnDefinitions[0].Width = new GridLength(left??0); + _rootGrid.ColumnDefinitions[1].Width = new GridLength(mid??0); + _rootGrid.ColumnDefinitions[2].Width = new GridLength(right??0); + } + + internal void SetIfUnset(AvaloniaProperty property, T value) + { + if (!IsSet(property)) + { + SetCurrentValue(property, value); + } } } \ No newline at end of file diff --git a/src/Ursa/Controls/Timeline/TimelinePanel.cs b/src/Ursa/Controls/Timeline/TimelinePanel.cs new file mode 100644 index 0000000..e496558 --- /dev/null +++ b/src/Ursa/Controls/Timeline/TimelinePanel.cs @@ -0,0 +1,75 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Ursa.Controls; + +public class TimelinePanel: Panel +{ + public static readonly StyledProperty ModeProperty = + Timeline.ModeProperty.AddOwner(); + + public TimelineDisplayMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + static TimelinePanel() + { + AffectsMeasure(ModeProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + double left = 0; + double right = 0; + double icon = 0; + double height = 0; + foreach (var child in Children) + { + child.Measure(availableSize); + if (child is TimelineItem t) + { + var doubles = t.GetWidth(); + left = Math.Max(left, doubles.left); + icon = Math.Max(icon, doubles.mid); + right = Math.Max(right, doubles.right); + } + height+=child.DesiredSize.Height; + } + return new Size(left+icon+right, height); + } + + protected override Size ArrangeOverride(Size finalSize) + { + + double left = 0, mid = 0, right = 0; + double height = 0; + foreach (var child in Children) + { + if (child is TimelineItem t) + { + var doubles = t.GetWidth(); + left = Math.Max(left, doubles.left); + mid = Math.Max(mid, doubles.mid); + right = Math.Max(right, doubles.right); + } + } + + Rect rect = new Rect(0, 0, left + mid + right, 0); + foreach (var child in Children) + { + if (child is TimelineItem t) + { + t.SetWidth(left, mid, right); + t.InvalidateArrange(); + rect = rect.WithHeight(t.DesiredSize.Height); + child.Arrange(rect); + rect = rect.WithY(rect.Y + t.DesiredSize.Height); + height+=t.DesiredSize.Height; + } + } + return new Size(left + mid + right, height); + } +} \ No newline at end of file