Merge branch 'feature/timeline' of https://github.com/chinware/AtomUI into feature/timeline

This commit is contained in:
liuyushuai 2024-10-15 23:45:28 +08:00
commit f8bc048407
13 changed files with 1018 additions and 72 deletions

View File

@ -1,4 +1,4 @@
namespace AtomUI.Theme
namespace AtomUI.Theme
{
internal class ControlThemeRegister
{
@ -114,4 +114,4 @@
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TreeViewTheme());
}
}
}
}

View File

@ -1,17 +1,17 @@
namespace AtomUI.Theme
{
internal class LanguageProviderRegister
{
internal static void Register()
{
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.DatePickerLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.DatePickerLang.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.Localization.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.Localization.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.PopupConfirmLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.PopupConfirmLang.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.TimePickerLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.TimePickerLang.zh_CN());
}
}
namespace AtomUI.Theme
{
internal class LanguageProviderRegister
{
internal static void Register()
{
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.DatePickerLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.DatePickerLang.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.Localization.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.Localization.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.PopupConfirmLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.PopupConfirmLang.zh_CN());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.TimePickerLang.en_US());
ThemeManager.Current.RegisterLanguageProvider(new AtomUI.Controls.TimePickerLang.zh_CN());
}
}
}

View File

@ -1,38 +1,38 @@
using AtomUI.Theme;
namespace AtomUI.Controls.DatePickerLang
{
public static class DatePickerLangResourceKey
{
public static readonly LanguageResourceKey Today = new LanguageResourceKey("DatePicker.Today", "AtomUI.Lang");
public static readonly LanguageResourceKey Now = new LanguageResourceKey("DatePicker.Now", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.Localization
{
public static class CommonLangResourceKey
{
public static readonly LanguageResourceKey OkText = new LanguageResourceKey("Common.OkText", "AtomUI.Lang");
public static readonly LanguageResourceKey CancelText = new LanguageResourceKey("Common.CancelText", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.PopupConfirmLang
{
public static class PopupConfirmLangResourceKey
{
public static readonly LanguageResourceKey OkText = new LanguageResourceKey("PopupConfirm.OkText", "AtomUI.Lang");
public static readonly LanguageResourceKey CancelText = new LanguageResourceKey("PopupConfirm.CancelText", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.TimePickerLang
{
public static class TimePickerLangResourceKey
{
public static readonly LanguageResourceKey AMText = new LanguageResourceKey("TimePicker.AMText", "AtomUI.Lang");
public static readonly LanguageResourceKey PMText = new LanguageResourceKey("TimePicker.PMText", "AtomUI.Lang");
public static readonly LanguageResourceKey Now = new LanguageResourceKey("TimePicker.Now", "AtomUI.Lang");
}
using AtomUI.Theme;
namespace AtomUI.Controls.DatePickerLang
{
public static class DatePickerLangResourceKey
{
public static readonly LanguageResourceKey Today = new LanguageResourceKey("DatePicker.Today", "AtomUI.Lang");
public static readonly LanguageResourceKey Now = new LanguageResourceKey("DatePicker.Now", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.Localization
{
public static class CommonLangResourceKey
{
public static readonly LanguageResourceKey OkText = new LanguageResourceKey("Common.OkText", "AtomUI.Lang");
public static readonly LanguageResourceKey CancelText = new LanguageResourceKey("Common.CancelText", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.PopupConfirmLang
{
public static class PopupConfirmLangResourceKey
{
public static readonly LanguageResourceKey OkText = new LanguageResourceKey("PopupConfirm.OkText", "AtomUI.Lang");
public static readonly LanguageResourceKey CancelText = new LanguageResourceKey("PopupConfirm.CancelText", "AtomUI.Lang");
}
}
namespace AtomUI.Controls.TimePickerLang
{
public static class TimePickerLangResourceKey
{
public static readonly LanguageResourceKey AMText = new LanguageResourceKey("TimePicker.AMText", "AtomUI.Lang");
public static readonly LanguageResourceKey PMText = new LanguageResourceKey("TimePicker.PMText", "AtomUI.Lang");
public static readonly LanguageResourceKey Now = new LanguageResourceKey("TimePicker.Now", "AtomUI.Lang");
}
}

View File

@ -1,4 +1,4 @@
namespace AtomUI.Theme
namespace AtomUI.Theme
{
internal class ControlTokenTypeRegister
{
@ -46,4 +46,4 @@
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.WindowToken));
}
}
}
}

View File

@ -1,4 +1,4 @@
using AtomUI.Theme.TokenSystem;
using AtomUI.Theme.TokenSystem;
namespace AtomUI.Theme.Styling
{
@ -703,4 +703,4 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey DefaultBackground = new TokenResourceKey("Window.DefaultBackground", "AtomUI.Token");
public static readonly TokenResourceKey DefaultForeground = new TokenResourceKey("Window.DefaultForeground", "AtomUI.Token");
}
}
}

View File

@ -18,8 +18,14 @@ public class ListBox : AvaloniaListBox
public static readonly StyledProperty<SizeType> SizeTypeProperty =
AvaloniaProperty.Register<ListBox, SizeType>(nameof(SizeType), SizeType.Middle);
#endregion
public static readonly StyledProperty<bool> DisabledItemHoverEffectProperty =
AvaloniaProperty.Register<ListBox, bool>(nameof(DisabledItemHoverEffect));
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ListBoxItem();
}
public SizeType SizeType
{
@ -33,8 +39,6 @@ public class ListBox : AvaloniaListBox
set => SetValue(DisabledItemHoverEffectProperty, value);
}
#endregion
protected override Size ArrangeOverride(Size finalSize)
{
return base.ArrangeOverride(finalSize.Deflate(new Thickness(BorderThickness.Left,
@ -62,4 +66,4 @@ public class ListBox : AvaloniaListBox
BindingPriority.Template,
new RenderScaleAwareThicknessConfigure(this));
}
}
}

View File

@ -0,0 +1,201 @@
using AtomUI.Data;
using AtomUI.Icon;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
namespace AtomUI.Controls;
public enum TimeLineMode
{
Left,
Right,
Alternate
}
public class Timeline : ItemsControl
{
#region
public static readonly StyledProperty<TimeLineMode> ModeProperty =
AvaloniaProperty.Register<Timeline, TimeLineMode>(nameof(Mode), TimeLineMode.Left);
public static readonly StyledProperty<string> PendingProperty =
AvaloniaProperty.Register<Timeline, string>(nameof(Pending), "");
public static readonly StyledProperty<bool> ReverseProperty =
AvaloniaProperty.Register<Timeline, bool>(nameof(Reverse), false);
public static readonly StyledProperty<PathIcon?> PendingIconProperty =
AvaloniaProperty.Register<Alert, PathIcon?>(nameof(PendingIcon));
public TimeLineMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
public string Pending
{
get => GetValue(PendingProperty);
set => SetValue(PendingProperty, value);
}
public bool Reverse
{
get => GetValue(ReverseProperty);
set { SetValue(ReverseProperty, value); }
}
public PathIcon? PendingIcon
{
get => GetValue(PendingIconProperty);
set => SetValue(PendingIconProperty, value);
}
#endregion
private TimelineItem? _pendingItem;
static Timeline()
{
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TimelineItem();
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TimelineItem>(item, out recycleKey);
}
protected override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
if (element is TimelineItem timelineItem)
{
BindUtils.RelayBind(this, ModeProperty, timelineItem, TimelineItem.ModeProperty);
BindUtils.RelayBind(this, ReverseProperty, timelineItem, TimelineItem.ReverseProperty);
BindUtils.RelayBind(this, ItemCountProperty, timelineItem, TimelineItem.CountProperty);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
OnReversePropertyChanged();
addPendingItem();
TokenResourceBinder.CreateGlobalTokenBinding(this, BorderThicknessProperty,
GlobalTokenResourceKey.BorderThickness,
BindingPriority.Template,
new RenderScaleAwareThicknessConfigure(this));
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ReverseProperty && VisualRoot is not null)
{
OnReversePropertyChanged();
}
if (change.Property == ItemCountProperty && VisualRoot is not null)
{
OnItemCountPropertyChanged();
}
if (change.Property == PendingProperty && VisualRoot is not null)
{
OnPendingPropertyChanged();
}
}
private void OnPendingPropertyChanged()
{
foreach (var item in Items)
{
if (item is TimelineItem timelineItem && timelineItem.IsPending)
{
Items.Remove(item);
break;
}
}
addPendingItem();
}
private void addPendingItem()
{
if (!String.IsNullOrEmpty(Pending))
{
if (_pendingItem is null)
{
_pendingItem = new TimelineItem();
var textBlock = new TextBlock();
if (PendingIcon is null)
{
PendingIcon = new PathIcon
{
Kind = "LoadingOutlined",
Width = 10,
Height = 10,
LoadingAnimation = IconAnimation.Spin,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Center,
};
}
_pendingItem.DotIcon = PendingIcon;
_pendingItem.IsPending = true;
_pendingItem.Content = textBlock;
BindUtils.RelayBind(this, PendingProperty, textBlock, TextBlock.TextProperty);
}
if (Reverse)
{
Items.Insert(0, _pendingItem);
}
else
{
Items.Add(_pendingItem);
}
}
}
private void OnItemCountPropertyChanged()
{
}
private void OnReversePropertyChanged()
{
var items = Items.Cast<object>().ToList();
items.Reverse();
Items.Clear();
foreach (var item in items)
{
Items.Add(item);
if (item is TimelineItem timelineItem)
{
timelineItem.Index = Items.IndexOf(item);
}
}
foreach (var item in items)
{
if (item is TimelineItem timelineItem)
{
timelineItem.Index = Items.IndexOf(item);
}
}
}
}

View File

@ -0,0 +1,361 @@
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
namespace AtomUI.Controls;
public class TimelineItem : ContentControl
{
internal const string PendingNodePC = ":pending";
internal const string ContentLeftPC = ":ContentLeft";
internal const string LabelLeftPC = ":labelLeft";
#region
public static readonly StyledProperty<string?> LabelProperty =
AvaloniaProperty.Register<TimelineItem, string?>(nameof(Label));
public static readonly StyledProperty<PathIcon?> DotIconProperty =
AvaloniaProperty.Register<Alert, PathIcon?>(nameof(DotIcon));
public static readonly StyledProperty<string> ColorProperty =
AvaloniaProperty.Register<TimelineItem, string>(nameof(Color), "blue");
public string? Label
{
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public PathIcon? DotIcon
{
get => GetValue(DotIconProperty);
set => SetValue(DotIconProperty, value);
}
public string Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
#endregion
#region
internal static readonly StyledProperty<int> IndexProperty =
AvaloniaProperty.Register<TimelineItem, int>(nameof(Index), 0);
internal static readonly StyledProperty<int> CountProperty =
AvaloniaProperty.Register<TimelineItem, int>(nameof(Count), 0);
internal static readonly StyledProperty<TimeLineMode> ModeProperty =
AvaloniaProperty.Register<TimelineItem, TimeLineMode>(nameof(Mode), TimeLineMode.Left);
internal static readonly StyledProperty<bool> HasLabelProperty =
AvaloniaProperty.Register<TimelineItem, bool>(nameof(HasLabel), false);
internal static readonly StyledProperty<bool> IsLastProperty =
AvaloniaProperty.Register<TimelineItem, bool>(nameof(IsLast), false);
internal static readonly StyledProperty<bool> ReverseProperty =
AvaloniaProperty.Register<TimelineItem, bool>(nameof(Reverse), false);
internal static readonly StyledProperty<bool> IsPendingProperty =
AvaloniaProperty.Register<TimelineItem, bool>(nameof(IsPending), false);
internal static readonly StyledProperty<HorizontalAlignment> ContentTextAlignProperty =
AvaloniaProperty.Register<TimelineItem, HorizontalAlignment>(nameof(ContentTextAlign),
HorizontalAlignment.Left);
internal static readonly StyledProperty<HorizontalAlignment> LabelTextAlignProperty =
AvaloniaProperty.Register<TimelineItem, HorizontalAlignment>(nameof(LabelTextAlign), HorizontalAlignment.Left);
internal int Index
{
get => GetValue(IndexProperty);
set => SetValue(IndexProperty, value);
}
internal int Count
{
get => GetValue(CountProperty);
set => SetValue(CountProperty, value);
}
internal TimeLineMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
internal bool HasLabel
{
get => GetValue(HasLabelProperty);
set => SetValue(HasLabelProperty, value);
}
internal bool IsLast
{
get => GetValue(IsLastProperty);
set => SetValue(IsLastProperty, value);
}
internal bool Reverse
{
get => GetValue(ReverseProperty);
set => SetValue(ReverseProperty, value);
}
internal bool IsPending
{
get => GetValue(IsPendingProperty);
set => SetValue(IsPendingProperty, value);
}
internal HorizontalAlignment ContentTextAlign
{
get => GetValue(ContentTextAlignProperty);
set => SetValue(ContentTextAlignProperty, value);
}
internal HorizontalAlignment LabelTextAlign
{
get => GetValue(LabelTextAlignProperty);
set => SetValue(LabelTextAlignProperty, value);
}
#endregion
protected internal int LabelIndex = 0;
protected internal int SplitIndex = 1;
protected internal int ContentIndex = 2;
private Grid? _gridContainer;
private DockPanel? _splitPanel;
private TextBlock? _labelBlock;
private ContentPresenter? _itemsContentPresenter;
private Border? _splitHeadPart;
private PathIcon? _dotPart;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ModeProperty || change.Property == CountProperty || change.Property == IndexProperty)
{
UpdateAll();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
HandleTemplateApplied(e.NameScope);
}
private void HandleTemplateApplied(INameScope scope)
{
_gridContainer = scope.Find<Grid>(TimelineItemTheme.GridPart);
_splitPanel = scope.Find<DockPanel>(TimelineItemTheme.SplitPanelPart);
_labelBlock = scope.Find<TextBlock>(TimelineItemTheme.LabelPart);
_itemsContentPresenter = scope.Find<ContentPresenter>(TimelineItemTheme.ItemsContentPresenterPart);
_splitHeadPart = scope.Find<Border>(TimelineItemTheme.SplitHeadPart);
_dotPart = scope.Find<PathIcon>(TimelineItemTheme.DotPart);
UpdateAll();
}
protected void UpdateAll()
{
CalculateIndex();
SetupShowInfo();
UpdatePseudoClasses();
}
protected void UpdatePseudoClasses()
{
PseudoClasses.Set(PendingNodePC, IsPending);
PseudoClasses.Set(ContentLeftPC, ContentIndex == 0);
PseudoClasses.Set(LabelLeftPC, LabelIndex == 0);
}
private void CalculateIndex()
{
if (VisualRoot is null || _gridContainer is null)
{
return;
}
if (Parent is Timeline timeline)
{
Index = timeline.Items.IndexOf(this);
IsLast = Index == timeline.Items.Count - 1;
HasLabel = false;
foreach (var child in timeline.Items)
{
if (child is TimelineItem item)
{
if (!string.IsNullOrEmpty(item.Label))
{
HasLabel = true;
break;
}
}
}
}
SplitIndex = 1;
LabelTextAlign = HorizontalAlignment.Right;
ContentTextAlign = HorizontalAlignment.Left;
if (Mode == TimeLineMode.Right || (Mode == TimeLineMode.Alternate && Index % 2 == 1))
{
LabelIndex = 2;
ContentIndex = 0;
LabelTextAlign = HorizontalAlignment.Left;
ContentTextAlign = HorizontalAlignment.Right;
}
else
{
LabelIndex = 0;
ContentIndex = 2;
LabelTextAlign = HorizontalAlignment.Right;
ContentTextAlign = HorizontalAlignment.Left;
}
_gridContainer.ColumnDefinitions[0].Width = GridLength.Star;
_gridContainer.ColumnDefinitions[2].Width = GridLength.Star;
if (!HasLabel)
{
if (Mode == TimeLineMode.Left)
{
LabelIndex = 0;
ContentIndex = 2;
ContentTextAlign = HorizontalAlignment.Left;
}
if (Mode == TimeLineMode.Right)
{
LabelIndex = 2;
ContentIndex = 0;
ContentTextAlign = HorizontalAlignment.Right;
}
_gridContainer.ColumnDefinitions[LabelIndex].Width = new GridLength(0);
}
}
private void SetupShowInfo()
{
if (_splitPanel is null || _itemsContentPresenter is null || _labelBlock is null || _gridContainer is null)
{
return;
}
Grid.SetColumn(_labelBlock, LabelIndex);
Grid.SetColumn(_itemsContentPresenter, ContentIndex);
Grid.SetColumn(_splitPanel, SplitIndex);
var dot = _splitPanel.Children[0];
var border = _splitPanel.Children[1] as Border;
var rect = border?.Child as Rectangle;
var isPendingItem = false;
MinHeight = 0;
if (Parent is Timeline timeline && !String.IsNullOrEmpty(timeline.Pending) && rect is not null &&
border is not null)
{
rect.StrokeDashArray = null;
isPendingItem = Reverse && Index == 0 || !Reverse && timeline.ItemCount - 2 == Index;
if (isPendingItem)
{
rect.StrokeDashArray = new AvaloniaList<double> { 0, 2 };
}
}
if (rect is not null)
{
rect.IsVisible = !IsLast;
}
if (IsLast || isPendingItem)
{
TokenResourceBinder.CreateGlobalTokenBinding(this, Layoutable.MinHeightProperty,
TimelineTokenResourceKey.LastItemContentMinHeight);
}
if (Color.StartsWith("#"))
{
try
{
var color = Avalonia.Media.Color.Parse(Color);
var brush = new SolidColorBrush(color);
if (_dotPart is not null && _dotPart.NormalFilledBrush is null)
{
_dotPart.NormalFilledBrush = brush;
}
if (_splitHeadPart is not null)
{
_splitHeadPart.BorderBrush = brush;
}
}
catch (Exception)
{
if (_dotPart is not null && _dotPart.NormalFilledBrush is null)
{
TokenResourceBinder.CreateGlobalTokenBinding(_dotPart, PathIcon.NormalFilledBrushProperty,
GlobalTokenResourceKey.ColorPrimary);
}
if (_splitHeadPart is not null)
{
TokenResourceBinder.CreateGlobalTokenBinding(_splitHeadPart, Border.BorderBrushProperty,
GlobalTokenResourceKey.ColorPrimary);
}
}
}
else
{
var tokenText = GlobalTokenResourceKey.ColorSuccess;
switch (Color)
{
case "blue":
tokenText = GlobalTokenResourceKey.ColorPrimary;
break;
case "green":
tokenText = GlobalTokenResourceKey.ColorSuccess;
break;
case "red":
tokenText = GlobalTokenResourceKey.ColorError;
break;
case "gray":
tokenText = GlobalTokenResourceKey.ColorTextDisabled;
break;
}
if (_dotPart is not null && _dotPart.NormalFilledBrush is null)
{
TokenResourceBinder.CreateGlobalTokenBinding(_dotPart, PathIcon.NormalFilledBrushProperty,
tokenText);
}
if (_splitHeadPart is not null)
{
TokenResourceBinder.CreateGlobalTokenBinding(_splitHeadPart, Border.BorderBrushProperty,
tokenText);
}
}
}
}

View File

@ -0,0 +1,195 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using HorizontalAlignment = Avalonia.Layout.HorizontalAlignment;
using VerticalAlignment = Avalonia.Layout.VerticalAlignment;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class TimelineItemTheme : BaseControlTheme
{
public const string GridPart = "PART_Grid";
public const string ItemsContentPresenterPart = "PART_ItemsContentPresenter";
public const string SplitPanelPart = "PART_PanelHead";
public const string SplitHeadPart = "PART_SplitHead";
public const string SplitLineBorderPart = "PART_SplitBorderLine";
public const string SplitLinePart = "PART_SplitLine";
public const string LabelPart = "PART_Label";
public const string DotPart = "PART_Dot";
public TimelineItemTheme() : base(typeof(TimelineItem))
{
}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<TimelineItem>((timelineItem, scope) =>
{
var grid = new Grid()
{
Name = GridPart,
ColumnDefinitions = new ColumnDefinitions()
{
new ColumnDefinition(GridLength.Star),
new ColumnDefinition(new GridLength(10)),
new ColumnDefinition(GridLength.Star)
},
RowDefinitions =
{
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Star),
},
};
grid.RegisterInNameScope(scope);
var labelBlock = new TextBlock()
{
Name = LabelPart,
VerticalAlignment = VerticalAlignment.Top,
};
labelBlock.RegisterInNameScope(scope);
CreateTemplateParentBinding(labelBlock, TextBlock.TextProperty, TimelineItem.LabelProperty);
CreateTemplateParentBinding(labelBlock, Visual.IsVisibleProperty, TimelineItem.HasLabelProperty);
CreateTemplateParentBinding(labelBlock, Layoutable.HorizontalAlignmentProperty,
TimelineItem.LabelTextAlignProperty);
grid.Children.Add(labelBlock);
var splitPanel = new DockPanel()
{
Name = SplitPanelPart,
Width = 10,
};
splitPanel.RegisterInNameScope(scope);
BuildDotIcon(splitPanel, timelineItem, scope);
var border = new Border
{
Name = SplitLineBorderPart,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
var verticalDashedLine = new Rectangle
{
Name = SplitLinePart,
StrokeLineCap = PenLineCap.Round,
Fill = Brushes.Transparent,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
verticalDashedLine.RegisterInNameScope(scope);
border.Child = verticalDashedLine;
splitPanel.Children.Add(border);
grid.Children.Add(splitPanel);
var contentPresenter = new ContentPresenter()
{
Name = ItemsContentPresenterPart,
};
contentPresenter.RegisterInNameScope(scope);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty,
ContentControl.ContentProperty);
CreateTemplateParentBinding(contentPresenter, Layoutable.HorizontalAlignmentProperty,
TimelineItem.ContentTextAlignProperty);
grid.Children.Add(contentPresenter);
return grid;
});
}
private void BuildDotIcon(DockPanel panel, TimelineItem timelineItem, INameScope scope)
{
if (timelineItem.DotIcon is not null)
{
timelineItem.DotIcon.Width = 10;
timelineItem.DotIcon.Height = 10;
timelineItem.DotIcon.Name = DotPart;
timelineItem.DotIcon.RegisterInNameScope(scope);
DockPanel.SetDock(timelineItem.DotIcon, Dock.Top);
panel.Children.Add(timelineItem.DotIcon);
}
else
{
var splitHead = new Border()
{
Width = 10,
Height = 10,
CornerRadius = new CornerRadius(5),
BorderThickness = new Thickness(3),
Name = SplitHeadPart,
};
splitHead.RegisterInNameScope(scope);
DockPanel.SetDock(splitHead, Dock.Top);
panel.Children.Add(splitHead);
}
}
protected override void BuildStyles()
{
// 分割线样式
var splitLineborderStyle = new Style(selector => selector.Nesting().Template().Name(SplitLineBorderPart));
splitLineborderStyle.Add(Layoutable.WidthProperty, TimelineTokenResourceKey.TailWidth);
Add(splitLineborderStyle);
var lineStyle = new Style(selector => selector.Nesting().Template().Child().OfType<Rectangle>());
lineStyle.Add(Shape.StrokeProperty, TimelineTokenResourceKey.TailColor);
lineStyle.Add(Layoutable.WidthProperty, TimelineTokenResourceKey.TailWidth);
lineStyle.Add(Shape.StrokeThicknessProperty, TimelineTokenResourceKey.TailWidth);
Add(lineStyle);
// 内容样式
var contentPresenterStyle =
new Style(selector => selector.Nesting().Template().Name(ItemsContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.FontSizeProperty, TimelineTokenResourceKey.FontSize);
contentPresenterStyle.Add(ContentPresenter.PaddingProperty, TimelineTokenResourceKey.ItemPaddingBottom);
var contentPresenterLeftStyle =
new Style(selector => selector.Nesting().Template().Name(ItemsContentPresenterPart));
contentPresenterLeftStyle.Add(Layoutable.MarginProperty, TimelineTokenResourceKey.RightMargin);
var contentPresenterRightStyle =
new Style(selector => selector.Nesting().Template().Name(ItemsContentPresenterPart));
contentPresenterRightStyle.Add(Layoutable.MarginProperty, TimelineTokenResourceKey.LeftMargin);
var contentLeftStyle = new Style(selector => selector.Nesting().Class(TimelineItem.ContentLeftPC));
var contentRightStyle = new Style(selector => selector.Nesting().Not(x => x.Class(TimelineItem.ContentLeftPC)));
contentLeftStyle.Add(contentPresenterLeftStyle);
contentRightStyle.Add(contentPresenterRightStyle);
Add(contentPresenterStyle);
Add(contentLeftStyle);
Add(contentRightStyle);
// 标签样式
var labelStyle = new Style(selector => selector.Nesting().Template().Name(LabelPart));
labelStyle.Add(TextBlock.FontSizeProperty, TimelineTokenResourceKey.FontSize);
var labelLeftStyle = new Style(selector =>
selector.Nesting().Class(TimelineItem.LabelLeftPC).Template().Name(LabelPart));
var labelRightStyle = new Style(selector =>
selector.Nesting().Not(x => x.Class(TimelineItem.LabelLeftPC)).Template().Name(LabelPart));
labelLeftStyle.Add(TextBlock.PaddingProperty, TimelineTokenResourceKey.RightMargin);
labelRightStyle.Add(TextBlock.PaddingProperty, TimelineTokenResourceKey.LeftMargin);
Add(labelStyle);
Add(labelLeftStyle);
Add(labelRightStyle);
}
}

View File

@ -0,0 +1,80 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class TimelineTheme : BaseControlTheme
{
public const string FrameDecoratorPart = "PART_FrameDecorator";
public const string ScrollViewerPart = "PART_ScrollViewer";
public const string ItemsPresenterPart = "PART_ItemsPresenter";
public TimelineTheme() : base(typeof(Timeline)) { }
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<Timeline>((timeline, scope) =>
{
var frameBorder = new Border()
{
Name = FrameDecoratorPart
};
var itemsPresenter = BuildItemsPresenter(timeline, scope);
var scrollViewer = BuildScrollViewer(timeline, scope);
scrollViewer.Content = itemsPresenter;
frameBorder.Child = scrollViewer;
return frameBorder;
});
}
private ScrollViewer BuildScrollViewer(Timeline timeline, INameScope scope)
{
var scrollViewer = new ScrollViewer
{
Name = ScrollViewerPart
};
return scrollViewer;
}
private ItemsPresenter BuildItemsPresenter(Timeline timeline, INameScope scope)
{
var itemsPresenter = new ItemsPresenter
{
Name = ItemsPresenterPart,
};
itemsPresenter.RegisterInNameScope(scope);
return itemsPresenter;
}
protected override void BuildStyles()
{
BuildFixedStyle();
BuildCommonStyle();
}
private void BuildFixedStyle()
{
this.Add(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Disabled);
this.Add(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Auto);
this.Add(ScrollViewer.IsScrollChainingEnabledProperty, true);
}
private void BuildCommonStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(Border.BorderBrushProperty, GlobalTokenResourceKey.ColorBorder);
var frameStyle = new Style(selector => selector.Nesting().Template().Name(FrameDecoratorPart));
frameStyle.Add(Border.BackgroundProperty, GlobalTokenResourceKey.ColorBgContainer);
commonStyle.Add(frameStyle);
Add(commonStyle);
}
}

View File

@ -0,0 +1,105 @@
using AtomUI.Theme.TokenSystem;
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
[ControlDesignToken]
internal class TimelineToken : AbstractControlDesignToken
{
public const string ID = "Timeline";
public TimelineToken()
: this(ID) { }
protected TimelineToken(string id)
: base(id) { }
/// <summary>
/// Timeline 轨迹颜色
/// </summary>
public Color TailColor { get; set; }
/// <summary>
/// 轨迹宽度
/// </summary>
public double TailWidth { get; set; }
/// <summary>
/// 节点边框宽度
/// </summary>
public double DotBorderWidth { get; set; }
/// <summary>
/// 列表项文字选中颜色
/// </summary>
public Color ItemSelectedColor { get; set; }
/// <summary>
/// 节点背景色
/// </summary>
public Color DotBg { get; set; }
/// <summary>
/// 列表项背景色
/// </summary>
public Color ItemBgColor { get; set; }
/// <summary>
/// 时间项下间距
/// </summary>
public Thickness ItemPaddingBottom { get; set; }
/// <summary>
/// right margin
/// </summary>
public Thickness RightMargin { get; set; }
/// <summary>
/// left margin
/// </summary>
public Thickness LeftMargin { get; set; }
/// <summary>
/// 最后一个Item的Content最小高度
/// </summary>
public double LastItemContentMinHeight { get; set; }
/// <summary>
/// 字体大小
/// </summary>
public double FontSize { get; set; }
/// <summary>
/// item head size
/// </summary>
public double ItemHeadSize { get; set; }
/// <summary>
/// custom head size
/// </summary>
public double CustomHeadSize { get; set; }
internal override void CalculateFromAlias()
{
base.CalculateFromAlias();
TailColor = _globalToken.ColorSplit;
TailWidth = _globalToken.StyleToken.LineWidthBold;
DotBorderWidth = _globalToken.SeedToken.Wireframe
? _globalToken.StyleToken.LineWidthBold
: _globalToken.SeedToken.LineWidth * 3;
DotBg = _globalToken.ColorToken.ColorNeutralToken.ColorBgContainer;
ItemPaddingBottom = new Thickness(0, 0, 0, _globalToken.Padding * 1.25);
LeftMargin = new Thickness(_globalToken.Margin, 0, 0, 0);
RightMargin = new Thickness(0, 0, _globalToken.MarginSM, 0);
LastItemContentMinHeight = _globalToken.HeightToken.ControlHeightLG * 1.2;
FontSize = _globalToken.FontToken.FontSize;
ItemHeadSize = 10;
CustomHeadSize = _globalToken.FontToken.FontSize;
}
}

View File

@ -1,9 +1,9 @@
namespace AtomUI.Theme
{
internal class LanguageProviderRegister
{
internal static void Register()
{
}
}
namespace AtomUI.Theme
{
internal class LanguageProviderRegister
{
internal static void Register()
{
}
}
}

View File

@ -1,4 +1,4 @@
using AtomUI.Theme.TokenSystem;
using AtomUI.Theme.TokenSystem;
namespace AtomUI.Theme.Styling
{
@ -223,4 +223,4 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey MotionDurationSlow = new TokenResourceKey("MotionDurationSlow", "AtomUI.Token");
public static readonly TokenResourceKey MotionDurationVerySlow = new TokenResourceKey("MotionDurationVerySlow", "AtomUI.Token");
}
}
}