[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:
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.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Utilities;
|
||||
using Irihi.Avalonia.Shared.Helpers;
|
||||
Reference in New Issue
Block a user