Refactor ArrowDecoratedBox

This commit is contained in:
polarboy 2024-09-19 12:53:14 +08:00
parent e0fb335357
commit 9d219301f2
8 changed files with 148 additions and 133 deletions

View File

@ -23,4 +23,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -117,6 +117,9 @@ public class ArrowDecoratedBox : ContentControl,
internal static readonly StyledProperty<double> ArrowSizeProperty
= AvaloniaProperty.Register<ArrowDecoratedBox, double>(nameof(ArrowSize));
internal static readonly StyledProperty<Direction> ArrowDirectionProperty
= AvaloniaProperty.Register<ArrowDecoratedBox, Direction>(nameof(ArrowDirection));
/// <summary>
/// 箭头的大小
/// </summary>
@ -126,6 +129,12 @@ public class ArrowDecoratedBox : ContentControl,
set => SetValue(ArrowSizeProperty, value);
}
internal Direction ArrowDirection
{
get => GetValue(ArrowDirectionProperty);
set => SetValue(ArrowDirectionProperty, value);
}
#endregion
// 指针最顶点位置
@ -133,13 +142,19 @@ public class ArrowDecoratedBox : ContentControl,
private (double, double) _arrowVertexPoint;
internal (double, double) ArrowVertexPoint => GetArrowVertexPoint();
private Geometry? _arrowGeometry;
private Rect _contentRect;
private Rect _arrowRect;
private bool _needGenerateArrowVertexPoint = true;
private Border? _contentDecorator;
static ArrowDecoratedBox()
{
AffectsMeasure<ArrowDecoratedBox>(ArrowPositionProperty, IsShowArrowProperty);
AffectsMeasure<ArrowDecoratedBox>(IsShowArrowProperty);
}
public ArrowDecoratedBox()
{
IsShowArrow = true;
ArrowPosition = ArrowPosition.BottomEdgeAlignedLeft;
}
public static Direction GetDirection(ArrowPosition arrowPosition)
@ -179,17 +194,19 @@ public class ArrowDecoratedBox : ContentControl,
public Rect GetMaskBounds()
{
return GetContentRect(DesiredSize).Deflate(0.5);
if (_contentDecorator is not null)
{
return _contentDecorator.Bounds;
}
return Bounds;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
HandleTemplateApplied(e.NameScope);
}
private void HandleTemplateApplied(INameScope scope)
{
_contentDecorator = e.NameScope.Get<Border>(ArrowDecoratedBoxTheme.ContentDecoratorPart);
if (IsShowArrow)
{
BuildGeometry(true);
@ -219,14 +236,22 @@ public class ArrowDecoratedBox : ContentControl,
{
// 当开启的时候,但是还没有加入的渲染树,这个时候我们取不到 Token 需要在取值的时候重新生成一下
_needGenerateArrowVertexPoint = true;
}
if (VisualRoot is not null)
{
BuildGeometry(true);
_arrowRect = GetArrowRect(DesiredSize);
InvalidateArrange();
}
}
if (e.Property == ArrowPositionProperty)
{
ArrowDirection = GetDirection(ArrowPosition);
}
}
private void BuildGeometry(bool force = false)
@ -241,6 +266,7 @@ public class ArrowDecoratedBox : ContentControl,
{
if (IsShowArrow)
{
var direction = GetDirection(ArrowPosition);
var matrix = Matrix.CreateTranslation(-ArrowSize / 2, -ArrowSize / 2);
@ -263,97 +289,12 @@ public class ArrowDecoratedBox : ContentControl,
matrix *= Matrix.CreateRotation(MathUtils.Deg2Rad(180));
matrix *= Matrix.CreateTranslation(ArrowSize / 2, ArrowSize / 2);
}
matrix *= Matrix.CreateTranslation(_arrowRect.X, _arrowRect.Y);
_arrowGeometry!.Transform = new MatrixTransform(matrix);
context.DrawGeometry(Background, null, _arrowGeometry);
}
}
protected override Size MeasureOverride(Size availableSize)
{
var size = base.MeasureOverride(availableSize);
var targetWidth = size.Width;
var targetHeight = size.Height;
targetHeight = Math.Max(MinHeight, targetHeight);
if (IsShowArrow)
{
BuildGeometry();
var realArrowSize = Math.Min(_arrowGeometry!.Bounds.Size.Height, _arrowGeometry!.Bounds.Size.Width);
var direction = GetDirection(ArrowPosition);
if (direction == Direction.Left || direction == Direction.Right)
{
targetWidth += realArrowSize;
}
else
{
targetHeight += realArrowSize;
}
}
var targetSize = new Size(targetWidth, targetHeight);
_arrowRect = GetArrowRect(targetSize);
return targetSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var visualChildren = VisualChildren;
var visualCount = visualChildren.Count;
_contentRect = GetContentRect(finalSize);
for (var i = 0; i < visualCount; ++i)
{
var child = visualChildren[i];
if (child is Layoutable layoutable)
{
layoutable.Arrange(_contentRect);
}
}
return finalSize;
}
internal Rect GetContentRect(Size finalSize)
{
var offsetX = 0d;
var offsetY = 0d;
var targetWidth = finalSize.Width;
var targetHeight = finalSize.Height;
if (IsShowArrow)
{
var arrowSize = Math.Min(_arrowGeometry!.Bounds.Size.Height, _arrowGeometry!.Bounds.Size.Width) + 0.5;
var direction = GetDirection(ArrowPosition);
if (direction == Direction.Left || direction == Direction.Right)
{
targetWidth -= arrowSize;
}
else
{
targetHeight -= arrowSize;
}
if (direction == Direction.Right)
{
offsetX = 0.5;
}
else if (direction == Direction.Bottom)
{
offsetY = 0.5;
}
else if (direction == Direction.Top)
{
offsetY = arrowSize - 0.5;
}
else
{
offsetX = arrowSize - 0.5;
}
}
return new Rect(offsetX, offsetY, targetWidth, targetHeight);
}
private Rect GetArrowRect(Size finalSize)
{
var offsetX = 0d;
@ -371,6 +312,7 @@ public class ArrowDecoratedBox : ContentControl,
position == ArrowPosition.LeftEdgeAlignedTop ||
position == ArrowPosition.LeftEdgeAlignedBottom)
{
offsetX = 0.5d;
targetWidth = minValue;
targetHeight = maxValue;
if (position == ArrowPosition.Left)
@ -404,6 +346,7 @@ public class ArrowDecoratedBox : ContentControl,
position == ArrowPosition.TopEdgeAlignedLeft ||
position == ArrowPosition.TopEdgeAlignedRight)
{
offsetY = 0.5d;
if (position == ArrowPosition.TopEdgeAlignedLeft)
{
offsetX = maxValue;
@ -424,7 +367,7 @@ public class ArrowDecoratedBox : ContentControl,
position == ArrowPosition.RightEdgeAlignedTop ||
position == ArrowPosition.RightEdgeAlignedBottom)
{
offsetX = finalSize.Width - minValue;
offsetX = finalSize.Width - minValue - 1.0d;
if (position == ArrowPosition.Right)
{
offsetY = (finalSize.Height - maxValue) / 2;
@ -457,7 +400,7 @@ public class ArrowDecoratedBox : ContentControl,
}
else
{
offsetY = finalSize.Height - minValue;
offsetY = finalSize.Height - minValue - 1.0d;
targetWidth = maxValue;
targetHeight = minValue;
if (position == ArrowPosition.BottomEdgeAlignedLeft)

View File

@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
@ -13,8 +14,10 @@ namespace AtomUI.Controls;
[ControlThemeProvider]
internal class ArrowDecoratedBoxTheme : BaseControlTheme
{
public const string DecoratorPart = "PART_Decorator";
public const string ContentDecoratorPart = "PART_ContentDecorator";
public const string ContentLayoutPart = "PART_ContentLayout";
public const string ContentPresenterPart = "PART_ContentPresenter";
public const string ArrowContentPart = "PART_ArrowContent";
public ArrowDecoratedBoxTheme() : this(typeof(ArrowDecoratedBox))
{
@ -28,23 +31,38 @@ internal class ArrowDecoratedBoxTheme : BaseControlTheme
{
return new FuncControlTemplate<ArrowDecoratedBox>((box, scope) =>
{
var decorator = new Border
var contentLayout = new DockPanel()
{
Name = DecoratorPart,
Margin = new Thickness(0)
Name = ContentLayoutPart,
LastChildFill = true
};
decorator.RegisterInNameScope(scope);
var arrowContent = new Control()
{
Name = ArrowContentPart
};
CreateTemplateParentBinding(arrowContent, Border.IsVisibleProperty,
ArrowDecoratedBox.IsShowArrowProperty);
contentLayout.Children.Add(arrowContent);
arrowContent.RegisterInNameScope(scope);
decorator.Child = BuildContent(scope);
CreateTemplateParentBinding(decorator, Border.BackgroundSizingProperty,
var content = BuildContent(scope);
var contentDecorator = new Border
{
Name = ContentDecoratorPart,
Margin = new Thickness(0)
};
contentDecorator.RegisterInNameScope(scope);
; CreateTemplateParentBinding(contentDecorator, Border.BackgroundSizingProperty,
TemplatedControl.BackgroundSizingProperty);
CreateTemplateParentBinding(decorator, Border.BackgroundProperty, TemplatedControl.BackgroundProperty);
CreateTemplateParentBinding(decorator, Border.CornerRadiusProperty, TemplatedControl.CornerRadiusProperty);
CreateTemplateParentBinding(decorator, Decorator.PaddingProperty, TemplatedControl.PaddingProperty);
CreateTemplateParentBinding(contentDecorator, Border.BackgroundProperty, TemplatedControl.BackgroundProperty);
CreateTemplateParentBinding(contentDecorator, Border.CornerRadiusProperty, TemplatedControl.CornerRadiusProperty);
CreateTemplateParentBinding(contentDecorator, Decorator.PaddingProperty, TemplatedControl.PaddingProperty);
return decorator;
contentDecorator.Child = content;
contentLayout.Children.Add(contentDecorator);
return contentLayout;
});
}
@ -54,6 +72,7 @@ internal class ArrowDecoratedBoxTheme : BaseControlTheme
{
Name = ContentPresenterPart
};
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty, ContentControl.ContentProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentTemplateProperty,
ContentControl.ContentTemplateProperty);
@ -69,6 +88,43 @@ internal class ArrowDecoratedBoxTheme : BaseControlTheme
commonStyle.Add(TemplatedControl.PaddingProperty, ArrowDecoratedBoxTokenResourceKey.Padding);
commonStyle.Add(ArrowDecoratedBox.ArrowSizeProperty, ArrowDecoratedBoxTokenResourceKey.ArrowSize);
commonStyle.Add(TemplatedControl.CornerRadiusProperty, GlobalTokenResourceKey.BorderRadius);
BuildArrowDirectionStyle(commonStyle);
Add(commonStyle);
}
private void BuildArrowDirectionStyle(Style commonStyle)
{
var topDirectionStyle = new Style(selector => selector.Nesting().PropertyEquals(ArrowDecoratedBox.ArrowDirectionProperty, Direction.Top));
{
var arrowContentStyle = new Style(selector => selector.Nesting().Template().Name(ArrowContentPart));
arrowContentStyle.Add(DockPanel.DockProperty, Dock.Top);
arrowContentStyle.Add(Control.HeightProperty, ArrowDecoratedBoxTokenResourceKey.ArrowContentThickness);
topDirectionStyle.Add(arrowContentStyle);
}
commonStyle.Add(topDirectionStyle);
var rightDirectionStyle = new Style(selector => selector.Nesting().PropertyEquals(ArrowDecoratedBox.ArrowDirectionProperty, Direction.Right));
{
var arrowContentStyle = new Style(selector => selector.Nesting().Template().Name(ArrowContentPart));
arrowContentStyle.Add(DockPanel.DockProperty, Dock.Right);
arrowContentStyle.Add(Control.WidthProperty, ArrowDecoratedBoxTokenResourceKey.ArrowContentThickness);
rightDirectionStyle.Add(arrowContentStyle);
}
commonStyle.Add(rightDirectionStyle);
var bottomDirectionStyle = new Style(selector => selector.Nesting().PropertyEquals(ArrowDecoratedBox.ArrowDirectionProperty, Direction.Bottom));
{
var arrowContentStyle = new Style(selector => selector.Nesting().Template().Name(ArrowContentPart));
arrowContentStyle.Add(DockPanel.DockProperty, Dock.Bottom);
arrowContentStyle.Add(Control.HeightProperty, ArrowDecoratedBoxTokenResourceKey.ArrowContentThickness);
bottomDirectionStyle.Add(arrowContentStyle);
}
commonStyle.Add(bottomDirectionStyle);
var leftDirectionStyle = new Style(selector => selector.Nesting().PropertyEquals(ArrowDecoratedBox.ArrowDirectionProperty, Direction.Left));
{
var arrowContentStyle = new Style(selector => selector.Nesting().Template().Name(ArrowContentPart));
arrowContentStyle.Add(DockPanel.DockProperty, Dock.Left);
arrowContentStyle.Add(Control.WidthProperty, ArrowDecoratedBoxTokenResourceKey.ArrowContentThickness);
leftDirectionStyle.Add(arrowContentStyle);
}
commonStyle.Add(leftDirectionStyle);
}
}

View File

@ -13,6 +13,11 @@ public class ArrowDecoratedBoxToken : AbstractControlDesignToken
/// </summary>
public double ArrowSize { get; set; }
/// <summary>
/// 绘制箭头的厚度,跟位置有关
/// </summary>
public double ArrowContentThickness { get; set; }
/// <summary>
/// 默认的内边距
/// </summary>
@ -27,6 +32,7 @@ public class ArrowDecoratedBoxToken : AbstractControlDesignToken
{
base.CalculateFromAlias();
ArrowSize = _globalToken.SeedToken.SizePopupArrow / 1.3;
ArrowContentThickness = ArrowSize / 2;
Padding = new Thickness(_globalToken.PaddingXS);
}
}

View File

@ -6,6 +6,7 @@ using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Metadata;
@ -149,9 +150,9 @@ public class Flyout : PopupFlyoutBase
}
}
var placement = Placement;
var anchor = PlacementAnchor;
var gravity = PlacementGravity;
var placement = popup.Placement;
var anchor = popup.PlacementAnchor;
var gravity = popup.PlacementGravity;
if (flyoutPresenter is not null)
{
@ -178,11 +179,11 @@ public class Flyout : PopupFlyoutBase
protected internal override void NotifyPopupCreated(Popup popup)
{
base.NotifyPopupCreated(popup);
BindUtils.RelayBind(this, PlacementProperty, popup, Avalonia.Controls.Primitives.Popup.PlacementProperty);
BindUtils.RelayBind(this, PlacementProperty, popup, Popup.PlacementProperty);
BindUtils.RelayBind(this, PlacementAnchorProperty, popup,
Avalonia.Controls.Primitives.Popup.PlacementAnchorProperty);
Popup.PlacementAnchorProperty);
BindUtils.RelayBind(this, PlacementGravityProperty, popup,
Avalonia.Controls.Primitives.Popup.PlacementGravityProperty);
Popup.PlacementGravityProperty);
BindUtils.RelayBind(this, MaskShadowsProperty, popup, Popup.MaskShadowsProperty);
SetupArrowPosition(popup);
}

View File

@ -167,7 +167,7 @@ public class MenuFlyoutPresenter : MenuBase, IShadowMaskInfoProvider
{
if (_arrowDecoratedBox is not null)
{
var contentRect = _arrowDecoratedBox.GetContentRect(Bounds.Size);
var contentRect = _arrowDecoratedBox.GetMaskBounds();
var adjustedPos = _arrowDecoratedBox.TranslatePoint(contentRect.Position, this) ?? default;
return new Rect(adjustedPos, contentRect.Size);
}

View File

@ -41,6 +41,7 @@ namespace AtomUI.Theme.Styling
public static class ArrowDecoratedBoxTokenResourceKey
{
public static readonly TokenResourceKey ArrowSize = new TokenResourceKey("ArrowDecoratedBox.ArrowSize", "AtomUI.Token");
public static readonly TokenResourceKey ArrowContentThickness = new TokenResourceKey("ArrowDecoratedBox.ArrowContentThickness", "AtomUI.Token");
public static readonly TokenResourceKey Padding = new TokenResourceKey("ArrowDecoratedBox.Padding", "AtomUI.Token");
}

View File

@ -67,6 +67,7 @@ public class Popup : AvaloniaPopup
private bool _initialized;
private ManagedPopupPositionerInfo? _managedPopupPositioner;
protected bool _animating;
private bool _isNeedFlip = true;
// 当鼠标移走了,但是打开动画还没完成,我们需要记录下来这个信号
internal bool RequestCloseWhereAnimationCompleted { get; set; }
@ -147,9 +148,12 @@ public class Popup : AvaloniaPopup
"Unable to create shadow layer, top level for PlacementTarget is null.");
}
if (_isNeedFlip)
{
if (Placement != PlacementMode.Pointer && Placement != PlacementMode.Center)
{
AdjustPopupHostPosition(placementTarget);
AdjustPopupHostPosition(placementTarget!);
}
}
if (!_animating)
@ -291,8 +295,6 @@ public class Popup : AvaloniaPopup
/// <summary>
/// 在这里处理翻转问题
/// </summary>
/// <param name="popupHost"></param>
/// <param name="placementTarget"></param>
internal void AdjustPopupHostPosition(Control placementTarget)
{
var offsetX = HorizontalOffset;
@ -307,8 +309,7 @@ public class Popup : AvaloniaPopup
var direction = PopupUtils.GetDirection(Placement);
var topLevel = TopLevel.GetTopLevel(placementTarget)!;
if (Placement != PlacementMode.Center &&
Placement != PlacementMode.Pointer)
if (Placement != PlacementMode.Center && Placement != PlacementMode.Pointer)
{
// 计算是否 flip
var parameters = new PopupPositionerParameters();
@ -422,14 +423,20 @@ public class Popup : AvaloniaPopup
_animating = true;
var placementTarget = GetEffectivePlacementTarget();
Open();
if (Placement != PlacementMode.Pointer && Placement != PlacementMode.Center)
{
AdjustPopupHostPosition(placementTarget!);
_isNeedFlip = false;
}
var popupRoot = (Host as PopupRoot)!;
// 获取 popup 的具体位置,这个就是非常准确的位置,还有大小
// TODO 暂时只支持 WindowBase popup
popupRoot.Hide();
var popupOffset = popupRoot.PlatformImpl!.Position;
var offset = new Point(popupOffset.X, popupOffset.Y);
var placementTarget = GetEffectivePlacementTarget();
var topLevel = TopLevel.GetTopLevel(placementTarget);
var scaling = topLevel?.RenderScaling ?? 1.0;