[New Control] Descriptions (#791)
* feat: add ColumnWrapPanel. * feat: add Descriptions and DescriptionsItem controls * WIP: extract a LabeledContentControl for this particular need. * wip. * feat: setup demo. fix various initial status issue. * fix: fix a layout issue. * feat: improve demo. * feat: update demo. * fix: remove a redundant calculation. * feat: update per copilot feedback. * feat: add tests, fix a calculation issue. * feat: refactor to change the default label and value binding assignment logic. * feat: introduce orientation. * misc: format codes. * feat: clarify LabelPosition property. * feat: extract Font resources. * revert AvatarDemo. * feat: assign specific value to Font resources for testing. * misc: resolve copilot comment. --------- Co-authored-by: Zhang Dian <54255897+zdpcdt@users.noreply.github.com>
This commit is contained in:
@@ -15,20 +15,28 @@
|
|||||||
<vm:MainWindowViewModel />
|
<vm:MainWindowViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Button Content="???" Click="Button_OnClick"></Button>
|
<StackPanel.Styles>
|
||||||
<ContentControl Name="content">
|
<Style Selector="Button">
|
||||||
<ContentControl.DataTemplates>
|
<Setter Property="Margin" Value="10" />
|
||||||
<DataTemplate DataType="x:Int32">
|
</Style>
|
||||||
<u:Breadcrumb Separator="·" Classes="Margin">
|
</StackPanel.Styles>
|
||||||
<u:BreadcrumbItem Content="a" />
|
<u:ColumnWrapPanel Column="4" Width="500" Background="AntiqueWhite">
|
||||||
<u:BreadcrumbItem Content="b" />
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
<u:BreadcrumbItem Content="c" />
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
</u:Breadcrumb>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" Width="200" />
|
||||||
</DataTemplate>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
<DataTemplate DataType="x:Double">
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
<TextBlock Text="Hello World"/>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
</DataTemplate>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" Width="200" />
|
||||||
</ContentControl.DataTemplates>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" Width="150" />
|
||||||
</ContentControl>
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" Width="200" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" Width="200" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
<Button Content="Hello" Theme="{DynamicResource SolidButton}" />
|
||||||
|
</u:ColumnWrapPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -10,16 +10,4 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Button_OnClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (content.Content is int s)
|
|
||||||
{
|
|
||||||
content.Content = 1.1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
content.Content = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
124
demo/Ursa.Demo/Pages/DescriptionsDemo.axaml
Normal file
124
demo/Ursa.Demo/Pages/DescriptionsDemo.axaml
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
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:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels"
|
||||||
|
xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800"
|
||||||
|
x:DataType="vm:DescriptionsDemoViewModel"
|
||||||
|
x:Class="Ursa.Demo.Pages.DescriptionsDemo">
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:DescriptionsDemoViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="20">
|
||||||
|
<TextBlock Text="Descriptions support different orientations and item alignments. " />
|
||||||
|
<u:Form LabelPosition="Left">
|
||||||
|
<u:EnumSelector
|
||||||
|
Name="orientation"
|
||||||
|
u:FormItem.Label="Orientation"
|
||||||
|
EnumType="Orientation"
|
||||||
|
Value="{x:Static Orientation.Vertical}" />
|
||||||
|
<u:EnumSelector
|
||||||
|
Name="labelPosition"
|
||||||
|
u:FormItem.Label="Label Position"
|
||||||
|
EnumType="common:Position"
|
||||||
|
Value="{x:Static common:Position.Left}" />
|
||||||
|
<u:EnumSelector
|
||||||
|
Name="itemAlignment"
|
||||||
|
u:FormItem.Label="Item Alignment"
|
||||||
|
EnumType="common:ItemAlignment"
|
||||||
|
Value="{x:Static common:ItemAlignment.Center}" />
|
||||||
|
</u:Form>
|
||||||
|
<u:Descriptions
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ItemsSource="{Binding Items}"
|
||||||
|
LabelMemberBinding="{Binding Label}"
|
||||||
|
DisplayMemberBinding="{Binding Description}"
|
||||||
|
Orientation="{Binding #orientation.Value}"
|
||||||
|
LabelPosition="{Binding #labelPosition.Value}"
|
||||||
|
ItemAlignment="{Binding #itemAlignment.Value}"
|
||||||
|
MaxWidth="400" />
|
||||||
|
|
||||||
|
<TextBlock Text="Descriptions support XAML inline declaration. " />
|
||||||
|
<u:Descriptions
|
||||||
|
MaxWidth="400"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Orientation="{Binding #orientation.Value}"
|
||||||
|
LabelPosition="{Binding #labelPosition.Value}"
|
||||||
|
ItemAlignment="{Binding #itemAlignment.Value}">
|
||||||
|
<u:DescriptionsItem Label="实际用户数量" Content="1,480,000" />
|
||||||
|
<u:DescriptionsItem Label="7天留存">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="98%" />
|
||||||
|
<PathIcon
|
||||||
|
Theme="{DynamicResource InnerPathIcon}"
|
||||||
|
Classes="Small"
|
||||||
|
Margin="2 0 0 0"
|
||||||
|
Data="{DynamicResource SemiIconArrowUp}"
|
||||||
|
Foreground="{DynamicResource SemiGreen5}" />
|
||||||
|
</StackPanel>
|
||||||
|
</u:DescriptionsItem>
|
||||||
|
<u:DescriptionsItem Label="安全等级" Content="3级" />
|
||||||
|
<u:DescriptionsItem Label="垂类标签">
|
||||||
|
<Label Theme="{DynamicResource TagLabel}" Content="电商" />
|
||||||
|
</u:DescriptionsItem>
|
||||||
|
<u:DescriptionsItem Label="认证状态" Content="未认证" />
|
||||||
|
</u:Descriptions>
|
||||||
|
|
||||||
|
<TextBlock Text="Use ColumnWrapPanel to display horizontally" />
|
||||||
|
<u:Descriptions
|
||||||
|
ItemsSource="{Binding Items2}"
|
||||||
|
LabelMemberBinding="{Binding Label}"
|
||||||
|
ItemAlignment="Plain">
|
||||||
|
<u:Descriptions.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<u:ColumnWrapPanel Column="5" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</u:Descriptions.ItemsPanel>
|
||||||
|
<u:Descriptions.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock
|
||||||
|
Text="{Binding Description}"
|
||||||
|
Margin="0 0 8 0" />
|
||||||
|
</DataTemplate>
|
||||||
|
</u:Descriptions.ItemTemplate>
|
||||||
|
</u:Descriptions>
|
||||||
|
|
||||||
|
<TextBlock Text="Setting Orientation to Horizontal support Classes Small and Large. " />
|
||||||
|
<StackPanel>
|
||||||
|
<Border Theme="{DynamicResource CardBorder}">
|
||||||
|
<u:Descriptions
|
||||||
|
Classes="Small"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
LabelPosition="Top">
|
||||||
|
<u:DescriptionsItem Label="实际用户数量" Content="1,480,000" />
|
||||||
|
<u:DescriptionsItem Label="7天留存" Content="98%" />
|
||||||
|
<u:DescriptionsItem Label="安全等级" Content="3级" />
|
||||||
|
</u:Descriptions>
|
||||||
|
</Border>
|
||||||
|
<Border Theme="{DynamicResource CardBorder}">
|
||||||
|
<u:Descriptions
|
||||||
|
Orientation="Horizontal"
|
||||||
|
LabelPosition="Top">
|
||||||
|
<u:DescriptionsItem Label="实际用户数量" Content="1,480,000" />
|
||||||
|
<u:DescriptionsItem Label="7天留存" Content="98%" />
|
||||||
|
<u:DescriptionsItem Label="安全等级" Content="3级" />
|
||||||
|
</u:Descriptions>
|
||||||
|
</Border>
|
||||||
|
<Border Theme="{DynamicResource CardBorder}">
|
||||||
|
<u:Descriptions
|
||||||
|
Classes="Large"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
LabelPosition="Top">
|
||||||
|
<u:DescriptionsItem Label="实际用户数量" Content="1,480,000" />
|
||||||
|
<u:DescriptionsItem Label="7天留存" Content="98%" />
|
||||||
|
<u:DescriptionsItem Label="安全等级" Content="3级" />
|
||||||
|
</u:Descriptions>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</UserControl>
|
||||||
11
demo/Ursa.Demo/Pages/DescriptionsDemo.axaml.cs
Normal file
11
demo/Ursa.Demo/Pages/DescriptionsDemo.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.Pages;
|
||||||
|
|
||||||
|
public partial class DescriptionsDemo : UserControl
|
||||||
|
{
|
||||||
|
public DescriptionsDemo()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
demo/Ursa.Demo/ViewModels/DescriptionsDemoViewModel.cs
Normal file
38
demo/Ursa.Demo/ViewModels/DescriptionsDemoViewModel.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public partial class DescriptionsDemoViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public ObservableCollection<DescriptionItemViewModel> Items { get; set; }
|
||||||
|
public ObservableCollection<DescriptionItemViewModel> Items2 { get; set; }
|
||||||
|
|
||||||
|
public DescriptionsDemoViewModel()
|
||||||
|
{
|
||||||
|
Items = new ObservableCollection<DescriptionItemViewModel>()
|
||||||
|
{
|
||||||
|
new() { Label = "Actual Users", Description = "1,480,000" },
|
||||||
|
new() { Label = "7-day Retention", Description = "98%" },
|
||||||
|
new() { Label = "Security Level", Description = "III" },
|
||||||
|
new() { Label = "Category Tag", Description = "E-commerce" },
|
||||||
|
new() { Label = "Authorized State", Description = "Unauthorized" },
|
||||||
|
};
|
||||||
|
Items2 = new ObservableCollection<DescriptionItemViewModel>()
|
||||||
|
{
|
||||||
|
new() { Label = "抖音号", Description = "SemiDesign" },
|
||||||
|
new() { Label = "主播类型", Description = "自由主播" },
|
||||||
|
new() { Label = "安全等级", Description = "3级" },
|
||||||
|
new() { Label = "垂类标签", Description = "编程" },
|
||||||
|
new() { Label = "作品数量", Description = "88888888" },
|
||||||
|
new() { Label = "认证状态", Description = "这是一个很长很长很长很长很长很长很长很长很长的值" },
|
||||||
|
new() { Label = "上次直播时间", Description = "2024-05-01 12:00:00" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class DescriptionItemViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string? _label;
|
||||||
|
[ObservableProperty] private object? _description;
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ public partial class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyDatePicker => new DatePickerDemoViewModel(),
|
MenuKeys.MenuKeyDatePicker => new DatePickerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDateRangePicker => new DateRangePickerDemoViewModel(),
|
MenuKeys.MenuKeyDateRangePicker => new DateRangePickerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDateTimePicker => new DateTimePickerDemoViewModel(),
|
MenuKeys.MenuKeyDateTimePicker => new DateTimePickerDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyDescriptions => new DescriptionsDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
|
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDisableContainer => new DisableContainerDemoViewModel(),
|
MenuKeys.MenuKeyDisableContainer => new DisableContainerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
|
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ public class MenuViewModel : ViewModelBase
|
|||||||
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
||||||
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
||||||
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox },
|
||||||
new() { MenuHeader = "Multi AutoCompleteBox", Key = MenuKeys.MenuKeyMultiAutoCompleteBox, Status = "New" },
|
new()
|
||||||
|
{
|
||||||
|
MenuHeader = "Multi AutoCompleteBox", Key = MenuKeys.MenuKeyMultiAutoCompleteBox, Status = "New"
|
||||||
|
},
|
||||||
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
||||||
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
|
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
|
||||||
new() { MenuHeader = "PathPicker", Key = MenuKeys.MenuKeyPathPicker, Status = "New" },
|
new() { MenuHeader = "PathPicker", Key = MenuKeys.MenuKeyPathPicker, Status = "New" },
|
||||||
@@ -56,11 +59,17 @@ public class MenuViewModel : ViewModelBase
|
|||||||
MenuHeader = "Date & Time", Children = new ObservableCollection<MenuItemViewModel>
|
MenuHeader = "Date & Time", Children = new ObservableCollection<MenuItemViewModel>
|
||||||
{
|
{
|
||||||
new() { MenuHeader = "Date Picker", Key = MenuKeys.MenuKeyDatePicker, Status = "Updated" },
|
new() { MenuHeader = "Date Picker", Key = MenuKeys.MenuKeyDatePicker, Status = "Updated" },
|
||||||
new() { MenuHeader = "Date Range Picker", Key = MenuKeys.MenuKeyDateRangePicker, Status = "Updated" },
|
new()
|
||||||
|
{
|
||||||
|
MenuHeader = "Date Range Picker", Key = MenuKeys.MenuKeyDateRangePicker, Status = "Updated"
|
||||||
|
},
|
||||||
new() { MenuHeader = "Date Time Picker", Key = MenuKeys.MenuKeyDateTimePicker, Status = "Updated" },
|
new() { MenuHeader = "Date Time Picker", Key = MenuKeys.MenuKeyDateTimePicker, Status = "Updated" },
|
||||||
new() { MenuHeader = "Time Box", Key = MenuKeys.MenuKeyTimeBox },
|
new() { MenuHeader = "Time Box", Key = MenuKeys.MenuKeyTimeBox },
|
||||||
new() { MenuHeader = "Time Picker", Key = MenuKeys.MenuKeyTimePicker, Status = "Updated" },
|
new() { MenuHeader = "Time Picker", Key = MenuKeys.MenuKeyTimePicker, Status = "Updated" },
|
||||||
new() { MenuHeader = "Time Range Picker", Key = MenuKeys.MenuKeyTimeRangePicker, Status = "Updated" },
|
new()
|
||||||
|
{
|
||||||
|
MenuHeader = "Time Range Picker", Key = MenuKeys.MenuKeyTimeRangePicker, Status = "Updated"
|
||||||
|
},
|
||||||
new() { MenuHeader = "Clock", Key = MenuKeys.MenuKeyClock }
|
new() { MenuHeader = "Clock", Key = MenuKeys.MenuKeyClock }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -83,13 +92,14 @@ public class MenuViewModel : ViewModelBase
|
|||||||
new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.MenuKeyAspectRatioLayout },
|
new() { MenuHeader = "AspectRatioLayout", Key = MenuKeys.MenuKeyAspectRatioLayout },
|
||||||
new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP" },
|
new() { MenuHeader = "Avatar", Key = MenuKeys.MenuKeyAvatar, Status = "WIP" },
|
||||||
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
||||||
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner, Status = "Updated" },
|
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
|
||||||
|
new() { MenuHeader = "Descriptions", Key = MenuKeys.MenuKeyDescriptions, Status = "New" },
|
||||||
new() { MenuHeader = "Disable Container", Key = MenuKeys.MenuKeyDisableContainer },
|
new() { MenuHeader = "Disable Container", Key = MenuKeys.MenuKeyDisableContainer },
|
||||||
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
|
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
|
||||||
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
|
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
|
||||||
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
|
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
|
||||||
new() { MenuHeader = "ElasticWrapPanel", Key = MenuKeys.MenuKeyElasticWrapPanel },
|
new() { MenuHeader = "ElasticWrapPanel", Key = MenuKeys.MenuKeyElasticWrapPanel },
|
||||||
new() { MenuHeader = "Marquee", Key = MenuKeys.MenuKeyMarquee, Status = "New" },
|
new() { MenuHeader = "Marquee", Key = MenuKeys.MenuKeyMarquee },
|
||||||
new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer },
|
new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer },
|
||||||
new() { MenuHeader = "Scroll To", Key = MenuKeys.MenuKeyScrollToButton },
|
new() { MenuHeader = "Scroll To", Key = MenuKeys.MenuKeyScrollToButton },
|
||||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
|
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
|
||||||
@@ -117,6 +127,7 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyDatePicker = "DatePicker";
|
public const string MenuKeyDatePicker = "DatePicker";
|
||||||
public const string MenuKeyDateRangePicker = "DateRangePicker";
|
public const string MenuKeyDateRangePicker = "DateRangePicker";
|
||||||
public const string MenuKeyDateTimePicker = "DateTimePicker";
|
public const string MenuKeyDateTimePicker = "DateTimePicker";
|
||||||
|
public const string MenuKeyDescriptions = "Descriptions";
|
||||||
public const string MenuKeyDialog = "Dialog";
|
public const string MenuKeyDialog = "Dialog";
|
||||||
public const string MenuKeyDisableContainer = "DisableContainer";
|
public const string MenuKeyDisableContainer = "DisableContainer";
|
||||||
public const string MenuKeyDivider = "Divider";
|
public const string MenuKeyDivider = "Divider";
|
||||||
|
|||||||
158
src/Ursa.Themes.Semi/Controls/Descriptions.axaml
Normal file
158
src/Ursa.Themes.Semi/Controls/Descriptions.axaml
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:iri="https://irihi.tech/shared">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<StackPanel Margin="20" Spacing="20">
|
||||||
|
<Border Theme="{DynamicResource CardBorder}">
|
||||||
|
<u:Descriptions Orientation="Horizontal" LabelPosition="Top" ItemAlignment="Center">
|
||||||
|
<u:DescriptionsItem Label="实际用户数量" Content="1,480,000" />
|
||||||
|
<u:DescriptionsItem Label="7天留存" Content="98%" />
|
||||||
|
<u:DescriptionsItem Label="安全等级" Content="3级" />
|
||||||
|
</u:Descriptions>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
<ControlTheme x:Key="{x:Type u:Descriptions}" TargetType="u:Descriptions">
|
||||||
|
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:Descriptions">
|
||||||
|
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^:fixed-width">
|
||||||
|
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[Orientation=Horizontal]">
|
||||||
|
<Setter Property="ItemsPanel">
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<u:ColumnWrapPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type u:DescriptionsItem}" TargetType="u:DescriptionsItem">
|
||||||
|
<Style Selector="^:horizontal">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:DescriptionsItem">
|
||||||
|
<Grid Margin="{DynamicResource DescriptionsItemMargin}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Label
|
||||||
|
Name="{x:Static u:LabeledContentControl.PART_Label}"
|
||||||
|
Grid.Column="0"
|
||||||
|
Foreground="{DynamicResource DescriptionsKeyTextForeground}"
|
||||||
|
TextBlock.LineHeight="{DynamicResource DescriptionsLineHeight}"
|
||||||
|
Content="{TemplateBinding Label}"
|
||||||
|
ContentTemplate="{TemplateBinding LabelTemplate}"
|
||||||
|
Width="{TemplateBinding LabelWidth}" />
|
||||||
|
<TextBlock
|
||||||
|
Name="PART_Colon"
|
||||||
|
Grid.Column="1"
|
||||||
|
Text=":"
|
||||||
|
Foreground="{DynamicResource DescriptionsKeyTextForeground}"
|
||||||
|
Margin="{DynamicResource DescriptionsKeyPlainMargin}" />
|
||||||
|
<ContentPresenter
|
||||||
|
Name="{x:Static iri:PartNames.PART_ContentPresenter}"
|
||||||
|
Grid.Column="2"
|
||||||
|
TextBlock.LineHeight="{DynamicResource DescriptionsLineHeight}"
|
||||||
|
Foreground="{DynamicResource DescriptionsValueTextForeground}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
|
||||||
|
<Style Selector="^[ItemAlignment=Center]">
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||||
|
<Setter Property="Padding" Value="{DynamicResource DescriptionsKeyPadding}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ TextBlock#PART_Colon">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[ItemAlignment=Left]">
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
<Setter Property="Padding" Value="{DynamicResource DescriptionsKeyPadding}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ TextBlock#PART_Colon">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[ItemAlignment=Justify]">
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
<Setter Property="Padding" Value="{DynamicResource DescriptionsKeyPadding}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ TextBlock#PART_Colon">
|
||||||
|
<Setter Property="IsVisible" Value="False" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:vertical">
|
||||||
|
<Setter Property="iri:ClassHelper.ClassSource" Value="{Binding $parent[u:Descriptions]}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:DescriptionsItem">
|
||||||
|
<StackPanel>
|
||||||
|
<Label
|
||||||
|
Name="{x:Static u:LabeledContentControl.PART_Label}"
|
||||||
|
DataContext="{TemplateBinding Label}"
|
||||||
|
Foreground="{DynamicResource DescriptionsKeyTextForeground}"
|
||||||
|
Content="{TemplateBinding Label}"
|
||||||
|
ContentTemplate="{TemplateBinding LabelTemplate}"
|
||||||
|
Width="{TemplateBinding LabelWidth}" />
|
||||||
|
<ContentPresenter
|
||||||
|
Name="{x:Static iri:PartNames.PART_ContentPresenter}"
|
||||||
|
Foreground="{DynamicResource DescriptionsValueTextForeground}"
|
||||||
|
FontWeight="{DynamicResource DescriptionsValueFontWeight}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsKeyMediumMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsKeyMediumFontSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsValueMediumMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsValueMediumFontSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Small">
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsKeySmallMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsKeySmallFontSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsValueSmallMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsValueSmallFontSize}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^.Large">
|
||||||
|
<Style Selector="^ /template/ Label#PART_Label">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsKeyLargeMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsKeyLargeFontSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Margin" Value="{DynamicResource DescriptionsValueLargeMargin}" />
|
||||||
|
<Setter Property="FontSize" Value="{DynamicResource DescriptionsValueLargeFontSize}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<ResourceInclude Source="DatePicker.axaml" />
|
<ResourceInclude Source="DatePicker.axaml" />
|
||||||
<ResourceInclude Source="DateRangePicker.axaml" />
|
<ResourceInclude Source="DateRangePicker.axaml" />
|
||||||
<ResourceInclude Source="DateTimePicker.axaml" />
|
<ResourceInclude Source="DateTimePicker.axaml" />
|
||||||
|
<ResourceInclude Source="Descriptions.axaml" />
|
||||||
<ResourceInclude Source="Dialog.axaml" />
|
<ResourceInclude Source="Dialog.axaml" />
|
||||||
<ResourceInclude Source="DialogShared.axaml" />
|
<ResourceInclude Source="DialogShared.axaml" />
|
||||||
<ResourceInclude Source="DisableContainer.axaml" />
|
<ResourceInclude Source="DisableContainer.axaml" />
|
||||||
|
|||||||
4
src/Ursa.Themes.Semi/Themes/Dark/Descriptions.axaml
Normal file
4
src/Ursa.Themes.Semi/Themes/Dark/Descriptions.axaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<StaticResource x:Key="DescriptionsKeyTextForeground" ResourceKey="SemiColorText2" />
|
||||||
|
<StaticResource x:Key="DescriptionsValueTextForeground" ResourceKey="SemiColorText0" />
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||||
<ResourceInclude Source="Clock.axaml" />
|
<ResourceInclude Source="Clock.axaml" />
|
||||||
<ResourceInclude Source="DatePicker.axaml" />
|
<ResourceInclude Source="DatePicker.axaml" />
|
||||||
|
<ResourceInclude Source="Descriptions.axaml" />
|
||||||
<ResourceInclude Source="Dialog.axaml" />
|
<ResourceInclude Source="Dialog.axaml" />
|
||||||
<ResourceInclude Source="Divider.axaml" />
|
<ResourceInclude Source="Divider.axaml" />
|
||||||
<ResourceInclude Source="DualBadge.axaml" />
|
<ResourceInclude Source="DualBadge.axaml" />
|
||||||
|
|||||||
4
src/Ursa.Themes.Semi/Themes/Light/Descriptions.axaml
Normal file
4
src/Ursa.Themes.Semi/Themes/Light/Descriptions.axaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<StaticResource x:Key="DescriptionsKeyTextForeground" ResourceKey="SemiColorText2" />
|
||||||
|
<StaticResource x:Key="DescriptionsValueTextForeground" ResourceKey="SemiColorText0" />
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||||
<ResourceInclude Source="Clock.axaml" />
|
<ResourceInclude Source="Clock.axaml" />
|
||||||
<ResourceInclude Source="DatePicker.axaml" />
|
<ResourceInclude Source="DatePicker.axaml" />
|
||||||
|
<ResourceInclude Source="Descriptions.axaml" />
|
||||||
<ResourceInclude Source="Dialog.axaml" />
|
<ResourceInclude Source="Dialog.axaml" />
|
||||||
<ResourceInclude Source="Divider.axaml" />
|
<ResourceInclude Source="Divider.axaml" />
|
||||||
<ResourceInclude Source="DualBadge.axaml" />
|
<ResourceInclude Source="DualBadge.axaml" />
|
||||||
|
|||||||
27
src/Ursa.Themes.Semi/Themes/Shared/Descriptions.axaml
Normal file
27
src/Ursa.Themes.Semi/Themes/Shared/Descriptions.axaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<x:Double x:Key="DescriptionsLineHeight">20</x:Double>
|
||||||
|
<FontWeight x:Key="DescriptionsValueFontWeight">600</FontWeight>
|
||||||
|
<x:Double x:Key="DescriptionsKeySmallFontSize">12</x:Double>
|
||||||
|
<x:Double x:Key="DescriptionsValueSmallFontSize">16</x:Double>
|
||||||
|
<x:Double x:Key="DescriptionsKeyMediumFontSize">14</x:Double>
|
||||||
|
<x:Double x:Key="DescriptionsValueMediumFontSize">20</x:Double>
|
||||||
|
<x:Double x:Key="DescriptionsKeyLargeFontSize">14</x:Double>
|
||||||
|
<x:Double x:Key="DescriptionsValueLargeFontSize">28</x:Double>
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsValueFontWeight" ResourceKey="SemiFontWeightBold" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsKeySmallFontSize" ResourceKey="SemiFontSizeSmall" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsValueSmallFontSize" ResourceKey="SemiFontSizeHeader6" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsKeyMediumFontSize" ResourceKey="SemiFontSizeRegular" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsValueMediumFontSize" ResourceKey="SemiFontSizeHeader4" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsKeyLargeFontSize" ResourceKey="SemiFontSizeRegular" /> -->
|
||||||
|
<!-- <StaticResource x:Key="DescriptionsValueLargeFontSize" ResourceKey="SemiFontSizeHeader2" /> -->
|
||||||
|
|
||||||
|
<Thickness x:Key="DescriptionsKeyPadding">0 0 24 0</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsItemMargin">0 0 0 12</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsValueSmallMargin">0 0 48 0</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsValueMediumMargin">0 0 60 0</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsValueLargeMargin">0 0 80 0</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsKeySmallMargin">0</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsKeyMediumMargin">0 0 0 4</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsKeyLargeMargin">0 0 0 4</Thickness>
|
||||||
|
<Thickness x:Key="DescriptionsKeyPlainMargin">0 0 8 0</Thickness>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||||
<ResourceInclude Source="DatePicker.axaml" />
|
<ResourceInclude Source="DatePicker.axaml" />
|
||||||
<ResourceInclude Source="DateTimePicker.axaml" />
|
<ResourceInclude Source="DateTimePicker.axaml" />
|
||||||
|
<ResourceInclude Source="Descriptions.axaml" />
|
||||||
<ResourceInclude Source="Dialog.axaml" />
|
<ResourceInclude Source="Dialog.axaml" />
|
||||||
<ResourceInclude Source="DialogShared.axaml" />
|
<ResourceInclude Source="DialogShared.axaml" />
|
||||||
<ResourceInclude Source="Divider.axaml" />
|
<ResourceInclude Source="Divider.axaml" />
|
||||||
|
|||||||
27
src/Ursa/Common/ItemAlignment.cs
Normal file
27
src/Ursa/Common/ItemAlignment.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Ursa.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the alignment of items with header and content in a collection control.
|
||||||
|
/// </summary>
|
||||||
|
public enum ItemAlignment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The separate line of header and content is aligned. Header right aligned and Content left aligned.
|
||||||
|
/// </summary>
|
||||||
|
Center,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The separate line of header and content is aligned. Header left aligned and Content right aligned.
|
||||||
|
/// </summary>
|
||||||
|
Justify,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The separate line of header and content is aligned. Header left aligned and Content left aligned.
|
||||||
|
/// </summary>
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content docks to the right of Header.
|
||||||
|
/// </summary>
|
||||||
|
Plain,
|
||||||
|
}
|
||||||
98
src/Ursa/Common/LabeledContentControl.cs
Normal file
98
src/Ursa/Common/LabeledContentControl.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Metadata;
|
||||||
|
using Avalonia.Controls.Presenters;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LabeledContentControl is almost identical to HeaderedContentControl, but uses "Label" terminology instead of "Header".
|
||||||
|
/// This is to provide better semantic meaning of paired label and content.
|
||||||
|
/// Label part is recommended to be Label control for accessibility.
|
||||||
|
/// </summary>
|
||||||
|
[TemplatePart(PART_Label, typeof(Label))]
|
||||||
|
public abstract class LabeledContentControl: ContentControl
|
||||||
|
{
|
||||||
|
public const string PART_Label = "PART_Label";
|
||||||
|
|
||||||
|
public static readonly StyledProperty<object?> LabelProperty = AvaloniaProperty.Register<LabeledContentControl, object?>(
|
||||||
|
nameof(Label));
|
||||||
|
|
||||||
|
public object? Label
|
||||||
|
{
|
||||||
|
get => GetValue(LabelProperty);
|
||||||
|
set => SetValue(LabelProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IDataTemplate?> LabelTemplateProperty = AvaloniaProperty.Register<LabeledContentControl, IDataTemplate?>(
|
||||||
|
nameof(LabelTemplate));
|
||||||
|
|
||||||
|
public IDataTemplate? LabelTemplate
|
||||||
|
{
|
||||||
|
get => GetValue(LabelTemplateProperty);
|
||||||
|
set => SetValue(LabelTemplateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Label? LabelHost
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LabeledContentControl()
|
||||||
|
{
|
||||||
|
LabelProperty.Changed.AddClassHandler<LabeledContentControl>((x, e) => x.LabelChanged(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LabelChanged(AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OldValue is ILogical oldChild)
|
||||||
|
{
|
||||||
|
LogicalChildren.Remove(oldChild);
|
||||||
|
}
|
||||||
|
if (e.NewValue is ILogical newChild)
|
||||||
|
{
|
||||||
|
LogicalChildren.Add(newChild);
|
||||||
|
}
|
||||||
|
HookLabelToContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
LabelHost = e.NameScope.Find<Label>(PART_Label);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoaded(RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnLoaded(e);
|
||||||
|
HookLabelToContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void HookLabelToContent()
|
||||||
|
{
|
||||||
|
if (LabelHost is null) return;
|
||||||
|
// Set it directly if content is a control, this is faster than looking up logical tree.
|
||||||
|
if (Content is InputElement input)
|
||||||
|
{
|
||||||
|
LabelHost.Target = input;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var focusable =
|
||||||
|
Presenter?.GetSelfAndLogicalDescendants()
|
||||||
|
.OfType<InputElement>()
|
||||||
|
.FirstOrDefault(a => a.Focusable);
|
||||||
|
if (focusable is not null)
|
||||||
|
{
|
||||||
|
LabelHost.Target = focusable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
208
src/Ursa/Controls/Descriptions/Descriptions.cs
Normal file
208
src/Ursa/Controls/Descriptions/Descriptions.cs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Metadata;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
|
using Avalonia.Metadata;
|
||||||
|
using Ursa.Common;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
[PseudoClasses(PC_FixedWidth)]
|
||||||
|
public class Descriptions: ItemsControl
|
||||||
|
{
|
||||||
|
public const string PC_FixedWidth = ":fixed-width";
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IDataTemplate?> LabelTemplateProperty =
|
||||||
|
LabeledContentControl.LabelTemplateProperty.AddOwner<Descriptions>();
|
||||||
|
|
||||||
|
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||||
|
public IDataTemplate? LabelTemplate
|
||||||
|
{
|
||||||
|
get => GetValue(LabelTemplateProperty);
|
||||||
|
set => SetValue(LabelTemplateProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBinding?> LabelMemberBindingProperty = AvaloniaProperty.Register<Descriptions, IBinding?>(
|
||||||
|
nameof(LabelMemberBinding));
|
||||||
|
|
||||||
|
[AssignBinding]
|
||||||
|
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||||
|
public IBinding? LabelMemberBinding
|
||||||
|
{
|
||||||
|
get => GetValue(LabelMemberBindingProperty);
|
||||||
|
set => SetValue(LabelMemberBindingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Position> LabelPositionProperty =
|
||||||
|
DescriptionsItem.LabelPositionProperty.AddOwner<Descriptions>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the header relative to the content. Only Top and Left are supported.
|
||||||
|
/// </summary>
|
||||||
|
public Position LabelPosition
|
||||||
|
{
|
||||||
|
get => GetValue(LabelPositionProperty);
|
||||||
|
set => SetValue(LabelPositionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<GridLength> LabelWidthProperty =
|
||||||
|
AvaloniaProperty.Register<Descriptions, GridLength>(nameof(LabelWidth));
|
||||||
|
|
||||||
|
public GridLength LabelWidth
|
||||||
|
{
|
||||||
|
get => GetValue(LabelWidthProperty);
|
||||||
|
set => SetValue(LabelWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ItemAlignment> ItemAlignmentProperty =
|
||||||
|
DescriptionsItem.ItemAlignmentProperty.AddOwner<Descriptions>();
|
||||||
|
|
||||||
|
public ItemAlignment ItemAlignment
|
||||||
|
{
|
||||||
|
get => GetValue(ItemAlignmentProperty);
|
||||||
|
set => SetValue(ItemAlignmentProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Orientation> OrientationProperty =
|
||||||
|
AvaloniaProperty.Register<Descriptions, Orientation>(nameof(Orientation), Orientation.Vertical);
|
||||||
|
|
||||||
|
public Orientation Orientation
|
||||||
|
{
|
||||||
|
get => GetValue(OrientationProperty);
|
||||||
|
set => SetValue(OrientationProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Descriptions()
|
||||||
|
{
|
||||||
|
LabelWidthProperty.Changed.AddClassHandler<Descriptions>((x, args) => x.OnLabelWidthChanged(args));
|
||||||
|
ItemAlignmentProperty.Changed.AddClassHandler<Descriptions, ItemAlignment>((x, args) =>
|
||||||
|
x.OnItemAlignmentChanged(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemAlignmentChanged(AvaloniaPropertyChangedEventArgs<ItemAlignment> args)
|
||||||
|
{
|
||||||
|
PseudoClasses.Set(PC_FixedWidth, args.GetNewValue<ItemAlignment>() != ItemAlignment.Plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
PseudoClasses.Set(PC_FixedWidth, this.ItemAlignment != ItemAlignment.Plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
if (change.Property == LabelMemberBindingProperty)
|
||||||
|
{
|
||||||
|
if (change.NewValue != null && LabelTemplate != null)
|
||||||
|
throw new InvalidOperationException("Cannot set both LabelMemberBinding and LabelTemplate.");
|
||||||
|
_labelDisplayMemberItemTemplate = null;
|
||||||
|
RefreshContainers();
|
||||||
|
}
|
||||||
|
if (change.Property == LabelTemplateProperty)
|
||||||
|
{
|
||||||
|
if (change.NewValue != null && LabelMemberBinding != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot set both LabelMemberBinding and LabelTemplate.");
|
||||||
|
}
|
||||||
|
RefreshContainers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLabelWidthChanged(AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
foreach (var item in this.GetLogicalDescendants().OfType<DescriptionsItem>())
|
||||||
|
{
|
||||||
|
item.LabelWidth = LabelWidth.IsAbsolute ? LabelWidth.Value : double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||||
|
{
|
||||||
|
return new DescriptionsItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||||
|
{
|
||||||
|
base.PrepareContainerForItemOverride(container, item, index);
|
||||||
|
if (container is not DescriptionsItem descriptionItem) return;
|
||||||
|
if (container == item) return;
|
||||||
|
SetupBindings(descriptionItem, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||||
|
{
|
||||||
|
recycleKey = null;
|
||||||
|
if (item is not DescriptionsItem descriptionItem) return true;
|
||||||
|
SetupBindings(descriptionItem, null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupBindings(DescriptionsItem container, object? item)
|
||||||
|
{
|
||||||
|
var effectiveLabelTemplate = GetLabelTemplate();
|
||||||
|
if (effectiveLabelTemplate is not null && !container.IsSet(LabeledContentControl.LabelTemplateProperty))
|
||||||
|
{
|
||||||
|
container.LabelTemplate = effectiveLabelTemplate;
|
||||||
|
}
|
||||||
|
var effectiveContentTemplate = GetContentTemplate();
|
||||||
|
if (effectiveContentTemplate is not null && !container.IsSet(ContentControl.ContentTemplateProperty))
|
||||||
|
{
|
||||||
|
container.ContentTemplate = effectiveContentTemplate;
|
||||||
|
}
|
||||||
|
if (!container.IsSet(LabeledContentControl.LabelProperty))
|
||||||
|
{
|
||||||
|
container.Label = item;
|
||||||
|
}
|
||||||
|
if (!container.IsSet(LabelPositionProperty))
|
||||||
|
{
|
||||||
|
container[!LabelPositionProperty] = this[!LabelPositionProperty];
|
||||||
|
}
|
||||||
|
if (!container.IsSet(DescriptionsItem.ItemAlignmentProperty))
|
||||||
|
{
|
||||||
|
container[!DescriptionsItem.ItemAlignmentProperty] = this[!ItemAlignmentProperty];
|
||||||
|
}
|
||||||
|
if (!container.IsSet(DescriptionsItem.LabelWidthProperty))
|
||||||
|
{
|
||||||
|
container.LabelWidth = LabelWidth.IsAbsolute ? LabelWidth.Value : double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDataTemplate? _valueDisplayMemberItemTemplate;
|
||||||
|
private IDataTemplate? _labelDisplayMemberItemTemplate;
|
||||||
|
|
||||||
|
private IDataTemplate? GetContentTemplate()
|
||||||
|
{
|
||||||
|
IDataTemplate? itemTemplate = this.ItemTemplate;
|
||||||
|
if (itemTemplate != null)
|
||||||
|
return itemTemplate;
|
||||||
|
if (this._valueDisplayMemberItemTemplate == null)
|
||||||
|
{
|
||||||
|
IBinding? binding = this.DisplayMemberBinding;
|
||||||
|
if (binding != null)
|
||||||
|
_valueDisplayMemberItemTemplate =
|
||||||
|
new FuncDataTemplate<object>((o, s) => new TextBlock { [!TextBlock.TextProperty] = binding });
|
||||||
|
}
|
||||||
|
return _valueDisplayMemberItemTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDataTemplate? GetLabelTemplate()
|
||||||
|
{
|
||||||
|
IDataTemplate? itemTemplate = this.LabelTemplate;
|
||||||
|
if (itemTemplate != null)
|
||||||
|
return itemTemplate;
|
||||||
|
if (this._labelDisplayMemberItemTemplate == null)
|
||||||
|
{
|
||||||
|
IBinding? binding = this.LabelMemberBinding;
|
||||||
|
if (binding != null)
|
||||||
|
_labelDisplayMemberItemTemplate =
|
||||||
|
new FuncDataTemplate<object>((o, s) => new TextBlock { [!TextBlock.TextProperty] = binding });
|
||||||
|
}
|
||||||
|
return _labelDisplayMemberItemTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Ursa/Controls/Descriptions/DescriptionsItem.cs
Normal file
63
src/Ursa/Controls/Descriptions/DescriptionsItem.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Metadata;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Irihi.Avalonia.Shared.Common;
|
||||||
|
using Ursa.Common;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
[PseudoClasses(PseudoClassName.PC_Horizontal, PseudoClassName.PC_Vertical)]
|
||||||
|
public class DescriptionsItem: LabeledContentControl
|
||||||
|
{
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Position> LabelPositionProperty = AvaloniaProperty.Register<DescriptionsItem, Position>(
|
||||||
|
nameof(LabelPosition));
|
||||||
|
|
||||||
|
public Position LabelPosition
|
||||||
|
{
|
||||||
|
get => GetValue(LabelPositionProperty);
|
||||||
|
set => SetValue(LabelPositionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<ItemAlignment> ItemAlignmentProperty = AvaloniaProperty.Register<DescriptionsItem, ItemAlignment>(
|
||||||
|
nameof(ItemAlignment));
|
||||||
|
|
||||||
|
public ItemAlignment ItemAlignment
|
||||||
|
{
|
||||||
|
get => GetValue(ItemAlignmentProperty);
|
||||||
|
set => SetValue(ItemAlignmentProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> LabelWidthProperty = AvaloniaProperty.Register<DescriptionsItem, double>(
|
||||||
|
nameof(LabelWidth));
|
||||||
|
|
||||||
|
public double LabelWidth
|
||||||
|
{
|
||||||
|
get => GetValue(LabelWidthProperty);
|
||||||
|
set => SetValue(LabelWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DescriptionsItem()
|
||||||
|
{
|
||||||
|
LabelPositionProperty.Changed.AddClassHandler<DescriptionsItem, Position>((item, args)=> item.OnLabelPositionChanged(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
UpdatePositionPseudoClass(LabelPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLabelPositionChanged(AvaloniaPropertyChangedEventArgs<Position> args)
|
||||||
|
{
|
||||||
|
UpdatePositionPseudoClass(args.GetNewValue<Position>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePositionPseudoClass(Position newPosition)
|
||||||
|
{
|
||||||
|
PseudoClasses.Set(PseudoClassName.PC_Horizontal, newPosition is Position.Left or Position.Right);
|
||||||
|
PseudoClasses.Set(PseudoClassName.PC_Vertical, newPosition is Position.Top or Position.Bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
src/Ursa/Controls/Panels/ColumnWrapPanel.cs
Normal file
86
src/Ursa/Controls/Panels/ColumnWrapPanel.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Irihi.Avalonia.Shared.Helpers;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
public class ColumnWrapPanel : Panel, INavigableContainer
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<int> ColumnProperty = AvaloniaProperty.Register<ColumnWrapPanel, int>(
|
||||||
|
nameof(Column), int.MaxValue, validate: a => a > 0);
|
||||||
|
|
||||||
|
public int Column
|
||||||
|
{
|
||||||
|
get => GetValue(ColumnProperty);
|
||||||
|
set => SetValue(ColumnProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ColumnWrapPanel()
|
||||||
|
{
|
||||||
|
AffectsMeasure<ColumnWrapPanel>(ColumnProperty);
|
||||||
|
AffectsArrange<ColumnWrapPanel>(ColumnProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
double unit = availableSize.Width / Column;
|
||||||
|
double x = 0;
|
||||||
|
double y = 0;
|
||||||
|
double rowHeight = 0;
|
||||||
|
for (var i = 0; i < Children.Count; i++)
|
||||||
|
{
|
||||||
|
var child = Children[i];
|
||||||
|
child.Measure(availableSize);
|
||||||
|
var desiredSize = child.DesiredSize;
|
||||||
|
// calculate how many columns the child will take
|
||||||
|
int colSpan = (int)Math.Ceiling(desiredSize.Width / unit);
|
||||||
|
if (colSpan > Column) colSpan = Column; // limit to max columns
|
||||||
|
double childWidth = colSpan * unit;
|
||||||
|
if (MathHelpers.GreaterThan(x + childWidth, availableSize.Width)) // wrap to next row
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
y += rowHeight;
|
||||||
|
rowHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += childWidth;
|
||||||
|
rowHeight = Math.Max(rowHeight, desiredSize.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Size(availableSize.Width, y + rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
|
double unit = finalSize.Width / Column;
|
||||||
|
double x = 0;
|
||||||
|
double y = 0;
|
||||||
|
double rowHeight = 0;
|
||||||
|
for (var i = 0; i < Children.Count; i++)
|
||||||
|
{
|
||||||
|
var child = Children[i];
|
||||||
|
var desiredSize = child.DesiredSize;
|
||||||
|
var remainingWidth = finalSize.Width - x;
|
||||||
|
if (MathHelpers.GreaterThan(desiredSize.Width, remainingWidth))
|
||||||
|
{
|
||||||
|
x = 0;
|
||||||
|
y += rowHeight;
|
||||||
|
rowHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Arrange(new Rect(x, y, desiredSize.Width, desiredSize.Height));
|
||||||
|
int colSpan = (int)Math.Ceiling(desiredSize.Width / unit);
|
||||||
|
if (colSpan > Column) colSpan = Column; // limit to max columns
|
||||||
|
x += colSpan * unit;
|
||||||
|
rowHeight = Math.Max(rowHeight, desiredSize.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Size(finalSize.Width, y + rowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Utilities;
|
using Avalonia.Utilities;
|
||||||
using Irihi.Avalonia.Shared.Helpers;
|
using Irihi.Avalonia.Shared.Helpers;
|
||||||
183
tests/HeadlessTest.Ursa/Controls/DescriptionTests/Test.cs
Normal file
183
tests/HeadlessTest.Ursa/Controls/DescriptionTests/Test.cs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
|
using Ursa.Common;
|
||||||
|
using Ursa.Controls;
|
||||||
|
|
||||||
|
namespace HeadlessTest.Ursa.Controls.DescriptionTests;
|
||||||
|
|
||||||
|
public class Test
|
||||||
|
{
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Descriptions_LabelPosition_Propagates_To_DescriptionItems()
|
||||||
|
{
|
||||||
|
var descriptions = new Descriptions
|
||||||
|
{
|
||||||
|
LabelPosition = Position.Left,
|
||||||
|
ItemsSource = new[]
|
||||||
|
{
|
||||||
|
new { Label = "Name", Content = "John Doe" },
|
||||||
|
new { Label = "Age", Content = "30" }
|
||||||
|
},
|
||||||
|
LabelMemberBinding = new Binding("Label"),
|
||||||
|
};
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Content = descriptions
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var items = descriptions.GetLogicalChildren().OfType<DescriptionsItem>().ToList();
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(Position.Left, item.LabelPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions.LabelPosition = Position.Top;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(Position.Top, item.LabelPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Inline_Descriptions_LabelPosition_Overrides_Parent_Descriptions()
|
||||||
|
{
|
||||||
|
var descriptions = new Descriptions
|
||||||
|
{
|
||||||
|
LabelPosition = Position.Left,
|
||||||
|
Items =
|
||||||
|
{
|
||||||
|
new DescriptionsItem()
|
||||||
|
{
|
||||||
|
Label = "Name",
|
||||||
|
Content = "John Doe",
|
||||||
|
LabelPosition = Position.Top
|
||||||
|
},
|
||||||
|
new DescriptionsItem()
|
||||||
|
{
|
||||||
|
Label = "Age",
|
||||||
|
Content = "30",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Content = descriptions
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var items = descriptions.GetLogicalChildren().OfType<DescriptionsItem>().ToList();
|
||||||
|
Assert.Equal(Position.Top, items[0].LabelPosition);
|
||||||
|
Assert.Equal(Position.Left, items[1].LabelPosition);
|
||||||
|
|
||||||
|
descriptions.LabelPosition = Position.Top;
|
||||||
|
Assert.Equal(Position.Top, items[0].LabelPosition);
|
||||||
|
Assert.Equal(Position.Top, items[1].LabelPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Descriptions_ItemAlignment_Propagates_To_DescriptionItems()
|
||||||
|
{
|
||||||
|
var descriptions = new Descriptions
|
||||||
|
{
|
||||||
|
LabelPosition = Position.Left,
|
||||||
|
ItemsSource = new[]
|
||||||
|
{
|
||||||
|
new { Label = "Name", Content = "John Doe" },
|
||||||
|
new { Label = "Age", Content = "30" }
|
||||||
|
},
|
||||||
|
ItemAlignment = ItemAlignment.Center,
|
||||||
|
LabelMemberBinding = new Binding("Label"),
|
||||||
|
};
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Content = descriptions
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var items = descriptions.GetLogicalChildren().OfType<DescriptionsItem>().ToList();
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(ItemAlignment.Center, item.ItemAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions.ItemAlignment = ItemAlignment.Justify;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(ItemAlignment.Justify, item.ItemAlignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Inline_Descriptions_ItemAlignment_Overrides_Parent_Descriptions()
|
||||||
|
{
|
||||||
|
var descriptions = new Descriptions
|
||||||
|
{
|
||||||
|
ItemAlignment = ItemAlignment.Center,
|
||||||
|
Items =
|
||||||
|
{
|
||||||
|
new DescriptionsItem()
|
||||||
|
{
|
||||||
|
Label = "Name",
|
||||||
|
Content = "John Doe",
|
||||||
|
ItemAlignment = ItemAlignment.Justify
|
||||||
|
},
|
||||||
|
new DescriptionsItem()
|
||||||
|
{
|
||||||
|
Label = "Age",
|
||||||
|
Content = "30",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Content = descriptions
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Show();
|
||||||
|
|
||||||
|
var items = descriptions.GetLogicalChildren().OfType<DescriptionsItem>().ToList();
|
||||||
|
Assert.Equal(ItemAlignment.Justify, items[0].ItemAlignment);
|
||||||
|
Assert.Equal(ItemAlignment.Center, items[1].ItemAlignment);
|
||||||
|
|
||||||
|
descriptions.ItemAlignment = ItemAlignment.Left;
|
||||||
|
Assert.Equal(ItemAlignment.Justify, items[0].ItemAlignment);
|
||||||
|
Assert.Equal(ItemAlignment.Left, items[1].ItemAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public void Descriptions_LabelWidth_Propagates_To_DescriptionItems()
|
||||||
|
{
|
||||||
|
var descriptions = new Descriptions
|
||||||
|
{
|
||||||
|
LabelWidth = new GridLength(100),
|
||||||
|
ItemsSource = new[]
|
||||||
|
{
|
||||||
|
new { Label = "Name", Content = "John Doe" },
|
||||||
|
new { Label = "Age", Content = "30" }
|
||||||
|
},
|
||||||
|
LabelMemberBinding = new Binding("Label"),
|
||||||
|
};
|
||||||
|
var window = new Window
|
||||||
|
{
|
||||||
|
Content = descriptions
|
||||||
|
};
|
||||||
|
window.Show();
|
||||||
|
var items = descriptions.GetLogicalChildren().OfType<DescriptionsItem>().ToList();
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(100, item.LabelWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions.LabelWidth = new GridLength(150);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
Assert.Equal(150, item.LabelWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user