diff --git a/Toolkit.Avalonia/Toolkit.Avalonia.csproj b/Toolkit.Avalonia/Toolkit.Avalonia.csproj index 639aaaf..c625c17 100644 --- a/Toolkit.Avalonia/Toolkit.Avalonia.csproj +++ b/Toolkit.Avalonia/Toolkit.Avalonia.csproj @@ -5,7 +5,7 @@ enable - + diff --git a/Toolkit.Foundation/Toolkit.Foundation.csproj b/Toolkit.Foundation/Toolkit.Foundation.csproj index d356ddc..88f7c79 100644 --- a/Toolkit.Foundation/Toolkit.Foundation.csproj +++ b/Toolkit.Foundation/Toolkit.Foundation.csproj @@ -9,7 +9,7 @@ - + diff --git a/Toolkit.UI.Avalonia/Toolkit.UI.Avalonia.csproj b/Toolkit.UI.Avalonia/Toolkit.UI.Avalonia.csproj index 0929603..9f8742e 100644 --- a/Toolkit.UI.Avalonia/Toolkit.UI.Avalonia.csproj +++ b/Toolkit.UI.Avalonia/Toolkit.UI.Avalonia.csproj @@ -5,8 +5,8 @@ enable - - + + diff --git a/Toolkit.UI.Controls.Avalonia/ContentBadge/ContentBadge.cs b/Toolkit.UI.Controls.Avalonia/ContentBadge/ContentBadge.cs index 9b9261b..b064062 100644 --- a/Toolkit.UI.Controls.Avalonia/ContentBadge/ContentBadge.cs +++ b/Toolkit.UI.Controls.Avalonia/ContentBadge/ContentBadge.cs @@ -8,106 +8,138 @@ namespace Toolkit.UI.Controls.Avalonia; public class ContentBadge : ContentControl { - public static readonly StyledProperty BadgePathProperty = - AvaloniaProperty.Register(nameof(BadgePath)); - - public static readonly StyledProperty BadgeSizeProperty = - AvaloniaProperty.Register(nameof(BadgeSize), 14); + public static readonly StyledProperty BadgePathProperty = + AvaloniaProperty.Register(nameof(BadgePath)); public static readonly StyledProperty BadgePlacementProperty = AvaloniaProperty.Register(nameof(BadgePlacement), ContentBadgePlacement.BottomRight); + public static readonly StyledProperty BadgeSizeProperty = + AvaloniaProperty.Register(nameof(BadgeSize), 14); + + public static readonly StyledProperty IsBadgeVisibleProperty = + AvaloniaProperty.Register(nameof(IsBadgeVisible), true); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == BadgePathProperty || + change.Property == BadgeSizeProperty || + change.Property == IsBadgeVisibleProperty) + { + UpdateBadge(); + } + } + private ContentControl? badgeContent; + public string BadgePath + { + get => GetValue(BadgePathProperty); + set => SetValue(BadgePathProperty, value); + } + public ContentBadgePlacement BadgePlacement { get => GetValue(BadgePlacementProperty); set => SetValue(BadgePlacementProperty, value); } - public string? BadgePath - { - get => GetValue(BadgePathProperty); - set => SetValue(BadgePathProperty, value); - } - public double BadgeSize { get => GetValue(BadgeSizeProperty); set => SetValue(BadgeSizeProperty, value); } - public void UpdateClip() + public bool IsBadgeVisible { - if (Content is Control content && - badgeContent is not null && - BadgePath is { Length: > 0 } && - Geometry.Parse(BadgePath) is Geometry geometry) + get => GetValue(IsBadgeVisibleProperty); + set => SetValue(IsBadgeVisibleProperty, value); + } + + public void UpdateBadge() + { + if (Content is Control content && badgeContent is not null) { - double backgroundWidth = DesiredSize.Width; - double backgroundHeight = DesiredSize.Height; - - double scaleX = BadgeSize / geometry.Bounds.Width; - double scaleY = BadgeSize / geometry.Bounds.Height; - - double adjustedStrokeWidth = Math.Min(scaleX, scaleY) * 8; - - Geometry knockoutGeometry = geometry.GetWidenedGeometry(new Pen(new SolidColorBrush(Colors.Transparent), adjustedStrokeWidth)); - - TransformGroup transformGroup = new(); - transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY)); - - double scaledWidth = knockoutGeometry.Bounds.Width * scaleX; - double scaledHeight = knockoutGeometry.Bounds.Height * scaleY; - - double offsetX = 0; - double offsetY = 0; - - switch (BadgePlacement) + if (IsBadgeVisible && + BadgePath is { Length: > 0 } && + Geometry.Parse(BadgePath) is Geometry geometry) { - case ContentBadgePlacement.TopLeft: - offsetX = 0; - offsetY = 0; - break; - case ContentBadgePlacement.TopRight: - offsetX = backgroundWidth - scaledWidth; - offsetY = 0; - break; - case ContentBadgePlacement.BottomLeft: - offsetX = 0; - offsetY = backgroundHeight - scaledHeight; - break; - case ContentBadgePlacement.BottomRight: - offsetX = backgroundWidth - scaledWidth; - offsetY = backgroundHeight - scaledHeight; - break; + double backgroundWidth = DesiredSize.Width; + double backgroundHeight = DesiredSize.Height; + + double badgeWidth = geometry.Bounds.Width; + double badgeHeight = geometry.Bounds.Height; + double scale = BadgeSize / Math.Max(badgeWidth, badgeHeight); + + double scaleX = scale; + double scaleY = scale; + + double adjustedStrokeWidth = Math.Min(scaleX, scaleY) * 8; + + Geometry knockoutGeometry = geometry.GetWidenedGeometry(new Pen(new SolidColorBrush(Colors.Transparent), adjustedStrokeWidth)); + + TransformGroup transformGroup = new(); + transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY)); + + double scaledWidth = knockoutGeometry.Bounds.Width * scaleX; + double scaledHeight = knockoutGeometry.Bounds.Height * scaleY; + + double offsetX = 0; + double offsetY = 0; + + switch (BadgePlacement) + { + case ContentBadgePlacement.TopLeft: + offsetX = 0; + offsetY = 0; + break; + case ContentBadgePlacement.TopRight: + offsetX = backgroundWidth - scaledWidth; + offsetY = 0; + break; + case ContentBadgePlacement.BottomLeft: + offsetX = 0; + offsetY = backgroundHeight - scaledHeight; + break; + case ContentBadgePlacement.BottomRight: + offsetX = backgroundWidth - scaledWidth; + offsetY = backgroundHeight - scaledHeight; + break; + } + + transformGroup.Children.Add(new TranslateTransform(offsetX, offsetY)); + knockoutGeometry.Transform = transformGroup; + + CombinedGeometry combinedGeometry = new() + { + GeometryCombineMode = GeometryCombineMode.Exclude, + Geometry1 = new RectangleGeometry { Rect = new Rect(0, 0, backgroundWidth, backgroundHeight) }, + Geometry2 = knockoutGeometry + }; + + content.Clip = combinedGeometry; + + Geometry overlayGeometry = geometry.Clone(); + + TransformGroup overlayTransformGroup = new(); + overlayTransformGroup.Children.Add(new ScaleTransform(scaleX, scaleY)); + + overlayTransformGroup.Children.Add(new TranslateTransform(offsetX, offsetY)); + overlayGeometry.Transform = overlayTransformGroup; + + badgeContent.Content = new Path + { + Data = overlayGeometry, + Fill = Foreground + }; } - - transformGroup.Children.Add(new TranslateTransform(offsetX, offsetY)); - knockoutGeometry.Transform = transformGroup; - - CombinedGeometry combinedGeometry = new() + else { - GeometryCombineMode = GeometryCombineMode.Exclude, - Geometry1 = new RectangleGeometry { Rect = new Rect(0, 0, backgroundWidth, backgroundHeight) }, - Geometry2 = knockoutGeometry - }; - - content.Clip = combinedGeometry; - - Geometry overlayGeometry = geometry.Clone(); - - TransformGroup overlayTransformGroup = new(); - overlayTransformGroup.Children.Add(new ScaleTransform(scaleX, scaleY)); - - overlayTransformGroup.Children.Add(new TranslateTransform(offsetX, offsetY)); - overlayGeometry.Transform = overlayTransformGroup; - - badgeContent.Content = new Path - { - Data = overlayGeometry, - Fill = Foreground - }; + badgeContent.Content = null; + content.Clip = null; + } } } @@ -120,7 +152,7 @@ public class ContentBadge : protected override void OnSizeChanged(SizeChangedEventArgs args) { base.OnSizeChanged(args); - UpdateClip(); + UpdateBadge(); } } diff --git a/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.axaml b/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.axaml index a9ec77a..af6c31a 100644 --- a/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.axaml +++ b/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.axaml @@ -13,7 +13,44 @@ 6 40 40 - + + + + + + + + + + + + + + + + + + + + + @@ -34,21 +71,28 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> - - - - + + + + + + @@ -116,18 +160,17 @@ - - + - - + + - - + - - + + diff --git a/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.cs b/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.cs index a8f2b9d..ee07333 100644 --- a/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.cs +++ b/Toolkit.UI.Controls.Avalonia/Overflow/Overflow.cs @@ -29,13 +29,14 @@ public class Overflow : private static readonly StyledProperty TemplateSettingsProperty = AvaloniaProperty.Register(nameof(TemplateSettings)); + private readonly ObservableCollection primaryCollection = []; private readonly ObservableCollection secondaryCollection = []; - private ListBox? primaryListBox; + private OverflowList? primaryListBox; - private ListBox? secondaryListBox; + private OverflowList? secondaryListBox; public Overflow() { @@ -59,7 +60,6 @@ public class Overflow : get => GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } - [InheritDataTypeFromItems(nameof(ItemsSource))] public IDataTemplate? ItemTemplate { @@ -83,10 +83,10 @@ public class Overflow : { base.OnApplyTemplate(args); - primaryListBox = args.NameScope.Get("PrimaryListBox"); + primaryListBox = args.NameScope.Get("PrimaryListBox"); primaryListBox?.SetValue(ItemsControl.ItemsSourceProperty, primaryCollection); - secondaryListBox = args.NameScope.Get("SecondaryListBox"); + secondaryListBox = args.NameScope.Get("SecondaryListBox"); secondaryListBox?.SetValue(ItemsControl.ItemsSourceProperty, secondaryCollection); InitializeCollections(); diff --git a/Toolkit.UI.Controls.Avalonia/Overflow/OverflowItem.cs b/Toolkit.UI.Controls.Avalonia/Overflow/OverflowItem.cs new file mode 100644 index 0000000..fd237ef --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/Overflow/OverflowItem.cs @@ -0,0 +1,45 @@ +using Avalonia; +using Avalonia.Controls; + +namespace Toolkit.UI.Controls.Avalonia; + +public class OverflowItem : + ListBoxItem +{ + public static readonly StyledProperty BadgeSizeProperty = + AvaloniaProperty.Register(nameof(BadgeSize), 14); + + public static readonly StyledProperty IsBadgeVisibleProperty = + AvaloniaProperty.Register(nameof(IsBadgeVisible), true); + + public static readonly StyledProperty BadgePathProperty = + AvaloniaProperty.Register(nameof(BadgePath)); + + public static readonly StyledProperty BadgePlacementProperty = + AvaloniaProperty.Register(nameof(BadgePlacement), ContentBadgePlacement.BottomRight); + + public string BadgePath + { + get => GetValue(BadgePathProperty); + set => SetValue(BadgePathProperty, value); + } + + public ContentBadgePlacement BadgePlacement + { + get => GetValue(BadgePlacementProperty); + set => SetValue(BadgePlacementProperty, value); + } + + public double BadgeSize + { + get => GetValue(BadgeSizeProperty); + set => SetValue(BadgeSizeProperty, value); + } + + public bool IsBadgeVisible + { + get => GetValue(IsBadgeVisibleProperty); + set => SetValue(IsBadgeVisibleProperty, value); + } +} + diff --git a/Toolkit.UI.Controls.Avalonia/Overflow/OverflowList.cs b/Toolkit.UI.Controls.Avalonia/Overflow/OverflowList.cs new file mode 100644 index 0000000..4948d61 --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/Overflow/OverflowList.cs @@ -0,0 +1,39 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace Toolkit.UI.Controls.Avalonia; + +public class OverflowList : + ListBox +{ + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + if (recycleKey is IDataTemplate itemContainerTemplate) + { + if (itemContainerTemplate.Build(item) is OverflowItem container) + { + return container; + } + } + + return new OverflowItem(); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + if (item is OverflowItem) + { + recycleKey = null; + return false; + } + + if (this.FindDataTemplate(item, ItemTemplate) is IDataTemplate itemContainerTemplate) + { + recycleKey = itemContainerTemplate; + return true; + } + + recycleKey = DefaultRecycleKey; + return true; + } +} \ No newline at end of file diff --git a/Toolkit.UI.Controls.Avalonia/Overflow/TemplateListBox.cs b/Toolkit.UI.Controls.Avalonia/Overflow/TemplateListBox.cs new file mode 100644 index 0000000..bc81382 --- /dev/null +++ b/Toolkit.UI.Controls.Avalonia/Overflow/TemplateListBox.cs @@ -0,0 +1,43 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace Toolkit.UI.Controls.Avalonia; + +public class TemplateListBox : + ListBox +{ + protected override Type StyleKeyOverride => + typeof(ListBox); + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + if (recycleKey is IDataTemplate itemContainerTemplate) + { + if (itemContainerTemplate.Build(item) is ListBoxItem container) + { + return container; + } + } + + return new ListBoxItem(); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + if (item is ListBoxItem) + { + recycleKey = null; + return false; + } + + if (this.FindDataTemplate(item, ItemTemplate) is IDataTemplate itemContainerTemplate) + { + recycleKey = itemContainerTemplate; + return true; + } + + recycleKey = DefaultRecycleKey; + return true; + } +} + diff --git a/Toolkit.UI.Controls.Avalonia/SettingsExpander/SettingsExpander.cs b/Toolkit.UI.Controls.Avalonia/SettingsExpander/SettingsExpander.cs index ecde79e..1d8ccd3 100644 --- a/Toolkit.UI.Controls.Avalonia/SettingsExpander/SettingsExpander.cs +++ b/Toolkit.UI.Controls.Avalonia/SettingsExpander/SettingsExpander.cs @@ -1,6 +1,4 @@ using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; namespace Toolkit.UI.Controls.Avalonia; diff --git a/Toolkit.UI.Controls.Avalonia/Toolkit.UI.Controls.Avalonia.csproj b/Toolkit.UI.Controls.Avalonia/Toolkit.UI.Controls.Avalonia.csproj index 405709c..552a0c8 100644 --- a/Toolkit.UI.Controls.Avalonia/Toolkit.UI.Controls.Avalonia.csproj +++ b/Toolkit.UI.Controls.Avalonia/Toolkit.UI.Controls.Avalonia.csproj @@ -12,7 +12,7 @@ - +