diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 5a76eb9..1d8954e 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -6,6 +6,7 @@ public static class MenuKeys public const string MenuKeyBanner = "Banner"; public const string MenuKeyButtonGroup = "ButtonGroup"; public const string MenuKeyDivider = "Divider"; + public const string MenuKeyDualBadge = "DualBadge"; public const string MenuKeyIpBox = "IPv4Box"; public const string MenuKeyLoading = "Loading"; public const string MenuKeyNavigation = "Navigation"; diff --git a/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml b/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml new file mode 100644 index 0000000..e1d85ef --- /dev/null +++ b/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml @@ -0,0 +1,210 @@ + + + + M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM14 7C14 8.10457 13.1046 9 12 9C10.8954 9 10 8.10457 10 7C10 5.89543 10.8954 5 12 5C13.1046 5 14 5.89543 14 7ZM9 10.75C9 10.3358 9.33579 10 9.75 10H12.5C13.0523 10 13.5 10.4477 13.5 11V16.5H14.25C14.6642 16.5 15 16.8358 15 17.25C15 17.6642 14.6642 18 14.25 18H9.75C9.33579 18 9 17.6642 9 17.25C9 16.8358 9.33579 16.5 9.75 16.5H10.5V11.5H9.75C9.33579 11.5 9 11.1642 9 10.75Z + + + + + + + + + + + + 2.4k + + + + + + 2.4k + + + 2.4k + + + + + + 2.4k + + + + + + + 2.4k + + + + + + 2.4k + + + 2.4k + + + + + + 2.4k + + + + + + + 2.4k + + + + + + 2.4k + + + 2.4k + + + + + + 2.4k + + + + + + + 2.4K + + + + + + 2.4K + + + 2.4K + + + + + + 2.4K + + + + + Red + Pink + Purple + Violet + Indigo + Blue + LightBlue + Cyan + Teal + Green + LightGreen + Lime + Yellow + Amber + Orange + Grey + + + Red + Pink + Purple + Violet + Indigo + Blue + LightBlue + Cyan + Teal + Green + LightGreen + Lime + Yellow + Amber + Orange + Grey + + + Red + Pink + Purple + Violet + Indigo + Blue + LightBlue + Cyan + Teal + Green + LightGreen + Lime + Yellow + Amber + Orange + Grey + + + Red + Pink + Purple + Violet + Indigo + Blue + LightBlue + Cyan + Teal + Green + LightGreen + Lime + Yellow + Amber + Orange + Grey + + + + \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml.cs b/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml.cs new file mode 100644 index 0000000..6db9c2f --- /dev/null +++ b/demo/Ursa.Demo/Pages/DualBadgeDemo.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Ursa.Demo.Pages; + +public partial class DualBadgeDemo : UserControl +{ + public DualBadgeDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/DualBadgeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/DualBadgeDemoViewModel.cs new file mode 100644 index 0000000..bc992fc --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/DualBadgeDemoViewModel.cs @@ -0,0 +1,5 @@ +namespace Ursa.Demo.ViewModels; + +public class DualBadgeDemoViewModel : ViewModelBase +{ +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 6641f1f..54109ae 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -28,6 +28,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyBanner => new BannerDemoViewModel(), MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), + MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(), MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(), MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(), MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 9351ad9..011375b 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -15,6 +15,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner }, new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup }, new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider }, + new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge }, new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation }, diff --git a/src/Ursa.Themes.Semi/Controls/DualBadge.axaml b/src/Ursa.Themes.Semi/Controls/DualBadge.axaml new file mode 100644 index 0000000..9a9e949 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/DualBadge.axaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index bbf2752..dee9b3c 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -5,6 +5,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Dark/DualBadge.axaml b/src/Ursa.Themes.Semi/Themes/Dark/DualBadge.axaml new file mode 100644 index 0000000..851dc91 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Dark/DualBadge.axaml @@ -0,0 +1,145 @@ + + White + White + White + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml index c9f2bea..aa85239 100644 --- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml @@ -5,6 +5,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Light/DualBadge.axaml b/src/Ursa.Themes.Semi/Themes/Light/DualBadge.axaml new file mode 100644 index 0000000..851dc91 --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Light/DualBadge.axaml @@ -0,0 +1,145 @@ + + White + White + White + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml index c9f2bea..aa85239 100644 --- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml @@ -5,6 +5,7 @@ + diff --git a/src/Ursa.Themes.Semi/Themes/Shared/DualBadge.axaml b/src/Ursa.Themes.Semi/Themes/Shared/DualBadge.axaml new file mode 100644 index 0000000..7a55daa --- /dev/null +++ b/src/Ursa.Themes.Semi/Themes/Shared/DualBadge.axaml @@ -0,0 +1,19 @@ + + 4 + 12 + 1 + 4 2 + Left + Center + False + False + 14 + 14 + + 0 + 4 0 + + 0 + 12 6 + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml index 777a0a4..7bf0b4d 100644 --- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml +++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml @@ -5,6 +5,7 @@ + diff --git a/src/Ursa.Themes.Semi/Ursa.Themes.Semi.csproj b/src/Ursa.Themes.Semi/Ursa.Themes.Semi.csproj index 5bb0d73..88fc18f 100644 --- a/src/Ursa.Themes.Semi/Ursa.Themes.Semi.csproj +++ b/src/Ursa.Themes.Semi/Ursa.Themes.Semi.csproj @@ -1,6 +1,6 @@ - + netstandard2.0 @@ -13,11 +13,11 @@ - + - + diff --git a/src/Ursa/Controls/DualBadge.cs b/src/Ursa/Controls/DualBadge.cs new file mode 100644 index 0000000..7ffd789 --- /dev/null +++ b/src/Ursa/Controls/DualBadge.cs @@ -0,0 +1,96 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Media; + +namespace Ursa.Controls; + +[PseudoClasses(PC_IconEmpty, PC_HeaderEmpty, PC_ContentEmpty)] +[TemplatePart(PART_Icon, typeof(ContentPresenter))] +public class DualBadge : HeaderedContentControl +{ + public const string PC_IconEmpty = ":icon-empty"; + public const string PC_HeaderEmpty = ":header-empty"; + public const string PC_ContentEmpty = ":content-empty"; + public const string PART_HeaderPresenter = "PART_HeaderPresenter"; + public const string PART_ContentPresenter = "PART_ContentPresenter"; + public const string PART_Icon = "PART_Icon"; + + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + AvaloniaProperty.Register(nameof(IconTemplate)); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IconForegroundProperty = + AvaloniaProperty.Register(nameof(IconForeground)); + + public IBrush? IconForeground + { + get => GetValue(IconForegroundProperty); + set => SetValue(IconForegroundProperty, value); + } + + public static readonly StyledProperty HeaderForegroundProperty = + AvaloniaProperty.Register(nameof(HeaderForeground)); + + public IBrush? HeaderForeground + { + get => GetValue(HeaderForegroundProperty); + set => SetValue(HeaderForegroundProperty, value); + } + + public static readonly StyledProperty HeaderBackgroundProperty = + AvaloniaProperty.Register(nameof(HeaderBackground)); + + public IBrush? HeaderBackground + { + get => GetValue(HeaderBackgroundProperty); + set => SetValue(HeaderBackgroundProperty, value); + } + + static DualBadge() + { + IconProperty.Changed.AddClassHandler((o, args) => o.OnIconChanged()); + HeaderProperty.Changed.AddClassHandler((o, args) => o.OnHeaderChanged()); + ContentProperty.Changed.AddClassHandler((o, args) => o.OnContentChanged()); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + OnIconChanged(); + OnHeaderChanged(); + OnContentChanged(); + } + + private void OnIconChanged() + { + PseudoClasses.Set(PC_IconEmpty, Icon is null); + } + + private void OnHeaderChanged() + { + PseudoClasses.Set(PC_HeaderEmpty, Header is null); + } + + private void OnContentChanged() + { + PseudoClasses.Set(PC_ContentEmpty, Content is null); + } +} \ No newline at end of file diff --git a/src/Ursa/Converters/CornerRadiusConverter.cs b/src/Ursa/Converters/CornerRadiusConverter.cs new file mode 100644 index 0000000..a5e041a --- /dev/null +++ b/src/Ursa/Converters/CornerRadiusConverter.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Converters; + +[Flags] +public enum CornerRadiusPosition +{ + TopLeft = 1, + TopRight = 2, + BottomLeft = 4, + BottomRight = 8, + Top = 3, + Left = 5, + Right = 10, + Bottom = 12, +} + +public class CornerRadiusIncludeConverter: IValueConverter +{ + public static CornerRadiusIncludeConverter TopLeft { get; } = new( CornerRadiusPosition.TopLeft ); + public static CornerRadiusIncludeConverter TopRight { get; } = new( CornerRadiusPosition.TopRight ); + public static CornerRadiusIncludeConverter BottomLeft { get; } = new( CornerRadiusPosition.BottomLeft ); + public static CornerRadiusIncludeConverter BottomRight { get; } = new( CornerRadiusPosition.BottomRight ); + public static CornerRadiusIncludeConverter Top { get; } = new( CornerRadiusPosition.Top ); + public static CornerRadiusIncludeConverter Left { get; } = new( CornerRadiusPosition.Left ); + public static CornerRadiusIncludeConverter Right { get; } = new( CornerRadiusPosition.Right ); + public static CornerRadiusIncludeConverter Bottom { get; } = new( CornerRadiusPosition.Bottom ); + + private readonly CornerRadiusPosition _position; + + public CornerRadiusIncludeConverter(CornerRadiusPosition position) + { + _position = position; + } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is CornerRadius r) + { + double topLeft = _position.HasFlag(CornerRadiusPosition.TopLeft) ? r.TopLeft : 0; + double topRight = _position.HasFlag(CornerRadiusPosition.TopRight) ? r.TopRight : 0; + double bottomLeft = _position.HasFlag(CornerRadiusPosition.BottomLeft) ? r.BottomLeft : 0; + double bottomRight = _position.HasFlag(CornerRadiusPosition.BottomRight) ? r.BottomRight : 0; + return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); + } + return AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public class CornerRadiusExcludeConverter : IValueConverter +{ + public static CornerRadiusExcludeConverter TopLeft { get; } = new( CornerRadiusPosition.TopLeft ); + public static CornerRadiusExcludeConverter TopRight { get; } = new( CornerRadiusPosition.TopRight ); + public static CornerRadiusExcludeConverter BottomLeft { get; } = new( CornerRadiusPosition.BottomLeft ); + public static CornerRadiusExcludeConverter BottomRight { get; } = new( CornerRadiusPosition.BottomRight ); + public static CornerRadiusExcludeConverter Top { get; } = new( CornerRadiusPosition.Top ); + public static CornerRadiusExcludeConverter Left { get; } = new( CornerRadiusPosition.Left ); + public static CornerRadiusExcludeConverter Right { get; } = new( CornerRadiusPosition.Right ); + public static CornerRadiusExcludeConverter Bottom { get; } = new( CornerRadiusPosition.Bottom ); + + private readonly CornerRadiusPosition _position; + + public CornerRadiusExcludeConverter(CornerRadiusPosition position) + { + _position = position; + } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is CornerRadius r) + { + double topLeft = _position.HasFlag(CornerRadiusPosition.TopLeft) ? 0 : r.TopLeft; + double topRight = _position.HasFlag(CornerRadiusPosition.TopRight) ? 0 : r.TopRight; + double bottomLeft = _position.HasFlag(CornerRadiusPosition.BottomLeft) ? 0 : r.BottomLeft; + double bottomRight = _position.HasFlag(CornerRadiusPosition.BottomRight) ? 0 : r.BottomRight; + return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft); + } + 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/src/Ursa/Converters/ThicknessConverter.cs b/src/Ursa/Converters/ThicknessConverter.cs new file mode 100644 index 0000000..6073cf0 --- /dev/null +++ b/src/Ursa/Converters/ThicknessConverter.cs @@ -0,0 +1,91 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Converters; + +[Flags] +public enum ThicknessPosition +{ + Left = 1, + Top = 2, + Right = 4, + Bottom = 8, + TopLeft = 3, + TopRight = 6, + BottomLeft = 9, + BottomRight = 12, +} + +public class ThicknessExcludeConverter: IValueConverter +{ + public static ThicknessExcludeConverter Left { get; } = new( ThicknessPosition.Left ); + public static ThicknessExcludeConverter Top { get; } = new( ThicknessPosition.Top ); + public static ThicknessExcludeConverter Right { get; } = new( ThicknessPosition.Right ); + public static ThicknessExcludeConverter Bottom { get; } = new( ThicknessPosition.Bottom ); + public static ThicknessExcludeConverter TopLeft { get; } = new( ThicknessPosition.TopLeft ); + public static ThicknessExcludeConverter TopRight { get; } = new( ThicknessPosition.TopRight ); + public static ThicknessExcludeConverter BottomLeft { get; } = new( ThicknessPosition.BottomLeft ); + public static ThicknessExcludeConverter BottomRight { get; } = new( ThicknessPosition.BottomRight ); + + private readonly ThicknessPosition _position; + public ThicknessExcludeConverter(ThicknessPosition position) + { + _position = position; + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Thickness t) + { + double left = _position.HasFlag(ThicknessPosition.Left) ? 0d: t.Left; + double top = _position.HasFlag(ThicknessPosition.Top) ? 0d : t.Top; + double right = _position.HasFlag(ThicknessPosition.Right) ? 0d : t.Right; + double bottom = _position.HasFlag(ThicknessPosition.Bottom) ? 0d : t.Bottom; + + return new Thickness(left, top, right, bottom); + } + return AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +public class ThicknessIncludeConverter: IValueConverter +{ + public static ThicknessIncludeConverter Left { get; } = new( ThicknessPosition.Left ); + public static ThicknessIncludeConverter Top { get; } = new( ThicknessPosition.Top ); + public static ThicknessIncludeConverter Right { get; } = new( ThicknessPosition.Right ); + public static ThicknessIncludeConverter Bottom { get; } = new( ThicknessPosition.Bottom ); + public static ThicknessIncludeConverter TopLeft { get; } = new( ThicknessPosition.TopLeft ); + public static ThicknessIncludeConverter TopRight { get; } = new( ThicknessPosition.TopRight ); + public static ThicknessIncludeConverter BottomLeft { get; } = new( ThicknessPosition.BottomLeft ); + public static ThicknessIncludeConverter BottomRight { get; } = new( ThicknessPosition.BottomRight ); + + private readonly ThicknessPosition _position; + public ThicknessIncludeConverter(ThicknessPosition position) + { + _position = position; + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Thickness t) + { + var left = _position.HasFlag(ThicknessPosition.Left) ? t.Left : 0d; + var top = _position.HasFlag(ThicknessPosition.Top) ? t.Top : 0d; + var right = _position.HasFlag(ThicknessPosition.Right) ? t.Right : 0d; + var bottom = _position.HasFlag(ThicknessPosition.Bottom) ? t.Bottom : 0d; + return new Thickness(left, top, right, bottom); + } + 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/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj index 01a32db..a6c5201 100644 --- a/src/Ursa/Ursa.csproj +++ b/src/Ursa/Ursa.csproj @@ -1,6 +1,6 @@ - + netstandard2.0 @@ -13,7 +13,7 @@ - +