Add Responsive maxTagCount support for Select

This commit is contained in:
chinboy
2025-11-03 14:26:24 +08:00
parent 3d050eb73b
commit fbc37ffbd2
11 changed files with 417 additions and 31 deletions
@@ -718,6 +718,9 @@ namespace AtomUI.Controls.DesignTokens
public static readonly TokenResourceKey OptionSelectedBg = new TokenResourceKey("Select.OptionSelectedBg");
public static readonly TokenResourceKey OptionSelectedColor = new TokenResourceKey("Select.OptionSelectedColor");
public static readonly TokenResourceKey OptionSelectedFontWeight = new TokenResourceKey("Select.OptionSelectedFontWeight");
public static readonly TokenResourceKey Padding = new TokenResourceKey("Select.Padding");
public static readonly TokenResourceKey PaddingLG = new TokenResourceKey("Select.PaddingLG");
public static readonly TokenResourceKey PaddingSM = new TokenResourceKey("Select.PaddingSM");
public static readonly TokenResourceKey PopupBorderRadius = new TokenResourceKey("Select.PopupBorderRadius");
public static readonly TokenResourceKey PopupBoxShadows = new TokenResourceKey("Select.PopupBoxShadows");
public static readonly TokenResourceKey PopupContentPadding = new TokenResourceKey("Select.PopupContentPadding");
+28 -4
View File
@@ -89,8 +89,11 @@ public class Select : TemplatedControl,
public static readonly StyledProperty<bool> IsShowMaxCountIndicatorProperty =
AvaloniaProperty.Register<Select, bool>(nameof(IsShowMaxCountIndicator));
public static readonly StyledProperty<int> MaxTagCountProperty =
AvaloniaProperty.Register<Select, int>(nameof(MaxTagCount), int.MaxValue);
public static readonly StyledProperty<int?> MaxTagCountProperty =
AvaloniaProperty.Register<Select, int?>(nameof(MaxTagCount));
public static readonly StyledProperty<bool?> IsResponsiveMaxTagCountProperty =
AvaloniaProperty.Register<Select, bool?>(nameof(IsResponsiveMaxTagCount));
public static readonly StyledProperty<string?> MaxTagPlaceholderProperty =
AvaloniaProperty.Register<Select, string?>(nameof(MaxTagPlaceholder));
@@ -264,12 +267,18 @@ public class Select : TemplatedControl,
set => SetValue(IsShowMaxCountIndicatorProperty, value);
}
public int MaxTagCount
public int? MaxTagCount
{
get => GetValue(MaxTagCountProperty);
set => SetValue(MaxTagCountProperty, value);
}
public bool? IsResponsiveMaxTagCount
{
get => GetValue(IsResponsiveMaxTagCountProperty);
set => SetValue(IsResponsiveMaxTagCountProperty, value);
}
public string? MaxTagPlaceholder
{
get => GetValue(MaxTagPlaceholderProperty);
@@ -652,7 +661,22 @@ public class Select : TemplatedControl,
}
else if (PseudoClasses.Contains(StdPseudoClass.Pressed))
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
var clickInTagCloseButton = false;
if (e.Source is Icon icon)
{
var parent = icon.FindAncestorOfType<IconButton>();
var tag = parent?.FindAncestorOfType<SelectTag>();
if (tag != null)
{
clickInTagCloseButton = true;
}
}
if (!clickInTagCloseButton)
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
}
e.Handled = true;
}
}
@@ -0,0 +1,189 @@
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace AtomUI.Controls;
internal class SelectMaxTagAwarePanel : StackPanel
{
private SelectRemainInfoTag? _infoTag;
private SelectSearchTextBox? _searchTextBox;
private int _remainCount = 0;
protected override Size MeasureOverride(Size availableSize)
{
var size = base.MeasureOverride(availableSize);
_remainCount = 0;
var effectiveWidth = 0d;
if (Children.Count > 0)
{
// 计算需要隐藏几个
var i = 0;
var tagInfoWidth = _infoTag?.DesiredSize.Width ?? 0d;
var offsetX = tagInfoWidth;
for (; i < Children.Count; i++)
{
var child = Children[i];
if (child != _infoTag)
{
offsetX += child.DesiredSize.Width + Spacing;
}
if (offsetX > availableSize.Width)
{
break;
}
effectiveWidth += child.DesiredSize.Width + Spacing;
}
if (offsetX > availableSize.Width)
{
_remainCount = Math.Max(0, Children.Count - 2 - i);
if (_remainCount > 0)
{
ConfigureInfoTagText(_remainCount);
}
effectiveWidth += tagInfoWidth;
}
}
return size.WithWidth(effectiveWidth);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Children.Count == 2)
{
_searchTextBox?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
}
else
{
var children = Children;
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
var spacing = Spacing;
Control? latestChild = null;
for (int i = 0, count = children.Count; i < count; ++i)
{
var child = children[i];
if (!child.IsVisible)
{
continue;
}
if (child is SelectRemainInfoTag)
{
continue;
}
if (_remainCount > 0)
{
if (i > count - _remainCount - 3)
{
child.Arrange(new Rect(int.MaxValue, 0, child.DesiredSize.Width, child.DesiredSize.Height));
continue;
}
}
if (fHorizontal)
{
rcChild = rcChild.WithX(rcChild.X + previousChildSize);
previousChildSize = child.DesiredSize.Width;
rcChild = rcChild.WithWidth(previousChildSize);
rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height));
previousChildSize += spacing;
}
else
{
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize);
rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
}
child.Arrange(rcChild);
latestChild = child;
}
if (_remainCount > 0 )
{
if (_infoTag != null)
{
if (latestChild != null) {
_infoTag.Arrange(new Rect(latestChild.Bounds.Right + Spacing, latestChild.Bounds.Y, _infoTag.DesiredSize.Width, _infoTag.DesiredSize.Height));
}
else
{
_infoTag.Arrange(new Rect(0, 0, _infoTag.DesiredSize.Width, _infoTag.DesiredSize.Height));
}
var offsetX = _infoTag.Bounds.Right + Spacing;
var searchBoxBoundsWidth = Math.Max(0, finalSize.Width - offsetX);
_searchTextBox?.Arrange(new Rect(offsetX, _infoTag.Bounds.Y, searchBoxBoundsWidth, finalSize.Height));
}
}
else
{
_infoTag?.Arrange(new Rect(int.MaxValue, 0, _infoTag.DesiredSize.Width, _infoTag.DesiredSize.Height));
if (latestChild != null)
{
var offsetX = latestChild.Bounds.Right - Spacing;
var searchBoxBoundsWidth = Math.Max(0, finalSize.Width - offsetX);
_searchTextBox?.Arrange(new Rect(offsetX, latestChild.Bounds.Y, searchBoxBoundsWidth, finalSize.Height));
}
}
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
}
return finalSize;
}
protected override void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.ChildrenChanged(sender, e);
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
if (oldItem is SelectRemainInfoTag)
{
_infoTag = null;
}
if (oldItem is SelectSearchTextBox)
{
_searchTextBox = null;
}
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
if (newItem is SelectRemainInfoTag selectTag)
{
_infoTag = selectTag;
ConfigureInfoTagText(0);
}
if (newItem is SelectSearchTextBox searchTextBox)
{
_searchTextBox = searchTextBox;
}
}
}
}
private void ConfigureInfoTagText(int remainCount)
{
if (_infoTag != null)
{
_infoTag.TagText = $"+ {remainCount} ...";
}
}
}
@@ -11,6 +11,8 @@ namespace AtomUI.Controls;
internal class SelectResultOptionsBox : TemplatedControl
{
#region 公共属性定义
public static readonly DirectProperty<SelectResultOptionsBox, IList?> SelectedOptionsProperty =
AvaloniaProperty.RegisterDirect<SelectResultOptionsBox, IList?>(
nameof(SelectedOptions),
@@ -29,6 +31,12 @@ internal class SelectResultOptionsBox : TemplatedControl
public static readonly StyledProperty<SizeType> SizeTypeProperty =
SizeTypeAwareControlProperty.SizeTypeProperty.AddOwner<SelectResultOptionsBox>();
public static readonly StyledProperty<int?> MaxTagCountProperty =
Select.MaxTagCountProperty.AddOwner<SelectResultOptionsBox>();
public static readonly StyledProperty<bool?> IsResponsiveMaxTagCountProperty =
Select.IsResponsiveMaxTagCountProperty.AddOwner<SelectResultOptionsBox>();
private IList? _selectedOptions;
public IList? SelectedOptions
@@ -60,9 +68,42 @@ internal class SelectResultOptionsBox : TemplatedControl
get => GetValue(SizeTypeProperty);
set => SetValue(SizeTypeProperty, value);
}
public int? MaxTagCount
{
get => GetValue(MaxTagCountProperty);
set => SetValue(MaxTagCountProperty, value);
}
public bool? IsResponsiveMaxTagCount
{
get => GetValue(IsResponsiveMaxTagCountProperty);
set => SetValue(IsResponsiveMaxTagCountProperty, value);
}
#endregion
#region 内部属性定义
public static readonly DirectProperty<SelectResultOptionsBox, bool> IsShowDefaultPanelProperty =
AvaloniaProperty.RegisterDirect<SelectResultOptionsBox, bool>(
nameof(IsShowDefaultPanel),
o => o.IsShowDefaultPanel,
(o, v) => o.IsShowDefaultPanel = v);
private bool _isShowDefaultPanel;
public bool IsShowDefaultPanel
{
get => _isShowDefaultPanel;
set => SetAndRaise(IsShowDefaultPanelProperty, ref _isShowDefaultPanel, value);
}
#endregion
private WrapPanel? _defaultPanel;
private SelectMaxTagAwarePanel? _maxCountAwarePanel;
private SelectSearchTextBox? _searchTextBox;
private SelectRemainInfoTag? _collapsedInfoTag;
private protected readonly Dictionary<object, IDisposable> _tagsBindingDisposables = new();
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -81,6 +122,24 @@ internal class SelectResultOptionsBox : TemplatedControl
{
ConfigureSearchTextReadOnly();
}
else if (change.Property == IsResponsiveMaxTagCountProperty ||
change.Property == MaxTagCountProperty)
{
if (IsResponsiveMaxTagCount == true)
{
SetCurrentValue(IsShowDefaultPanelProperty, false);
}
else
{
SetCurrentValue(IsShowDefaultPanelProperty, true);
}
}
else if (change.Property == IsShowDefaultPanelProperty)
{
_defaultPanel?.Children.Clear();
_maxCountAwarePanel?.Children.Clear();
HandleSelectedOptionsChanged();
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
@@ -99,16 +158,24 @@ internal class SelectResultOptionsBox : TemplatedControl
{
base.OnApplyTemplate(e);
_defaultPanel = e.NameScope.Find<WrapPanel>(SelectResultOptionsBoxThemeConstants.DefaultPanelPart);
_maxCountAwarePanel = e.NameScope.Find<SelectMaxTagAwarePanel>(SelectResultOptionsBoxThemeConstants.MaxCountAwarePanelPart);
_searchTextBox = new SelectSearchTextBox
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
_collapsedInfoTag = new SelectRemainInfoTag()
{
IsClosable = false
};
BindUtils.RelayBind(this, SizeTypeProperty, _searchTextBox, SizeTypeProperty);
if (IsSearchEnabled)
{
if (Mode == SelectMode.Multiple)
{
_defaultPanel?.Children.Add(_searchTextBox);
if (IsShowDefaultPanel)
{
_defaultPanel?.Children.Add(_searchTextBox);
}
}
}
@@ -118,36 +185,80 @@ internal class SelectResultOptionsBox : TemplatedControl
private void HandleSelectedOptionsChanged()
{
if (_defaultPanel != null)
if (_isShowDefaultPanel)
{
_searchTextBox?.Clear();
_defaultPanel.Children.Clear();
foreach (var entry in _tagsBindingDisposables)
if (_defaultPanel != null)
{
entry.Value.Dispose();
}
_tagsBindingDisposables.Clear();
if (_selectedOptions != null)
{
foreach (var item in _selectedOptions)
_searchTextBox?.Clear();
_defaultPanel.Children.Clear();
foreach (var entry in _tagsBindingDisposables)
{
if (item is SelectOption option)
entry.Value.Dispose();
}
_tagsBindingDisposables.Clear();
if (_selectedOptions != null)
{
foreach (var item in _selectedOptions)
{
var tag = new SelectTag
if (item is SelectOption option)
{
TagText = option.Header,
Option = option
};
_tagsBindingDisposables.Add(tag, BindUtils.RelayBind(this, SizeTypeProperty, tag, SizeTypeProperty));
_defaultPanel.Children.Add(tag);
var tag = new SelectTag
{
TagText = option.Header,
Option = option
};
_tagsBindingDisposables.Add(tag, BindUtils.RelayBind(this, SizeTypeProperty, tag, SizeTypeProperty));
_defaultPanel.Children.Add(tag);
}
}
}
}
if (_searchTextBox != null)
if (_searchTextBox != null)
{
_defaultPanel.Children.Add(_searchTextBox);
_searchTextBox.Focus();
}
}
}
else
{
if (_maxCountAwarePanel != null)
{
_defaultPanel.Children.Add(_searchTextBox);
_searchTextBox.Focus();
_searchTextBox?.Clear();
_maxCountAwarePanel.Children.Clear();
foreach (var entry in _tagsBindingDisposables)
{
entry.Value.Dispose();
}
_tagsBindingDisposables.Clear();
if (_selectedOptions != null)
{
foreach (var item in _selectedOptions)
{
if (item is SelectOption option)
{
var tag = new SelectTag
{
TagText = option.Header,
Option = option
};
_tagsBindingDisposables.Add(tag, BindUtils.RelayBind(this, SizeTypeProperty, tag, SizeTypeProperty));
_maxCountAwarePanel.Children.Add(tag);
}
}
}
if (_collapsedInfoTag != null)
{
_maxCountAwarePanel.Children.Add(_collapsedInfoTag);
}
if (_searchTextBox != null)
{
_maxCountAwarePanel.Children.Add(_searchTextBox);
_searchTextBox.Focus();
}
}
}
}
+5
View File
@@ -24,4 +24,9 @@ internal class SelectTag : Tag
CloseButton.IsPassthroughMouseEvent = true;
}
}
}
internal class SelectRemainInfoTag : SelectTag
{
protected override Type StyleKeyOverride { get; } = typeof(SelectTag);
}
+19
View File
@@ -126,6 +126,21 @@ internal class SelectToken : AbstractControlDesignToken
/// </summary>
public Thickness MultiModePaddingLG { get; set; }
/// <summary>
/// 输入框内边距
/// </summary>
public Thickness Padding { get; set; }
/// <summary>
/// 小号输入框内边距
/// </summary>
public Thickness PaddingSM { get; set; }
/// <summary>
/// 多选模式下的大号输入框内边距
/// </summary>
public Thickness PaddingLG { get; set; }
public SelectToken()
: base(ID)
{
@@ -180,5 +195,9 @@ internal class SelectToken : AbstractControlDesignToken
var multiPaddingRightLG = SharedToken.ControlPaddingHorizontal - lineWidth;
MultiModePaddingLG = new Thickness(multiPaddingVertical, multiPaddingVertical, multiPaddingRightLG, multiPaddingVertical);
Padding = new Thickness(multiPaddingRight, multiPaddingVertical);
PaddingSM = new Thickness(multiPaddingRightSM, multiPaddingVertical);
PaddingLG = new Thickness(multiPaddingRightLG, multiPaddingVertical);
}
}
@@ -32,6 +32,28 @@
</Style>
</Style>
</Style>
<Style Selector="^[IsSelectionEmpty=True]">
<Style Selector="^[SizeType=Large]">
<Style Selector="^ /template/ Border#PART_ContentFrame">
<Setter Property="Padding"
Value="{DynamicResource {x:Static atom:SelectTokenKey.PaddingLG}}" />
</Style>
</Style>
<Style Selector="^[SizeType=Middle]">
<Style Selector="^ /template/ Border#PART_ContentFrame">
<Setter Property="Padding"
Value="{DynamicResource {x:Static atom:SelectTokenKey.Padding}}" />
</Style>
</Style>
<Style Selector="^[SizeType=Small]">
<Style Selector="^ /template/ Border#PART_ContentFrame">
<Setter Property="Padding"
Value="{DynamicResource {x:Static atom:SelectTokenKey.Padding}}" />
</Style>
</Style>
</Style>
</Style>
<Style Selector="^[StyleVariant=Outline]">
@@ -7,14 +7,23 @@
<Setter Property="Template">
<ControlTemplate>
<Panel Name="RootLayout">
<atom:SelectWrapPanel Name="{x:Static atom:SelectResultOptionsBoxThemeConstants.DefaultPanelPart}"/>
<atom:SelectWrapPanel Name="{x:Static atom:SelectResultOptionsBoxThemeConstants.DefaultPanelPart}"
IsVisible="{TemplateBinding IsShowDefaultPanel}" />
<atom:SelectMaxTagAwarePanel
Name="{x:Static atom:SelectResultOptionsBoxThemeConstants.MaxCountAwarePanelPart}"
Orientation="Horizontal"
IsVisible="{TemplateBinding IsShowDefaultPanel, Converter={x:Static BoolConverters.Not}}" />
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ atom|SelectWrapPanel#PART_DefaultPanel">
<Setter Property="ItemSpacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}"/>
<Setter Property="LineSpacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}"/>
<Setter Property="ItemSpacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}" />
<Setter Property="LineSpacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}" />
</Style>
<Style Selector="^ /template/ atom|SelectMaxTagAwarePanel#PART_MaxCountAwarePanel">
<Setter Property="Spacing" Value="{DynamicResource {x:Static atom:SharedTokenKey.SpacingXXS}}" />
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -80,6 +80,8 @@
SelectedOptions="{TemplateBinding SelectedOptions}"
IsSearchEnabled="{TemplateBinding IsSearchEnabled}"
IsDropDownOpen="{TemplateBinding IsDropDownOpen}"
MaxTagCount="{TemplateBinding MaxTagCount}"
IsResponsiveMaxTagCount="{TemplateBinding IsResponsiveMaxTagCount}"
IsVisible="False"
Focusable="True"/>
</Panel>
@@ -15,4 +15,5 @@ internal static class SelectThemeConstants
internal static class SelectResultOptionsBoxThemeConstants
{
public const string DefaultPanelPart = "PART_DefaultPanel";
public const string MaxCountAwarePanelPart = "PART_MaxCountAwarePanel";
}
+2 -1
View File
@@ -183,7 +183,8 @@ public class Tag : TemplatedControl, IControlSharedTokenResourcesHost
StatusColorMap = new Dictionary<TagStatus, TagStatusCalcColor>();
AffectsMeasure<Tag>(BorderedProperty,
IconProperty,
IsClosableProperty);
IsClosableProperty,
TagTextProperty);
AffectsRender<Tag>(TagColorProperty,
ForegroundProperty,
BackgroundProperty,