Refactor tooltip

This commit is contained in:
chinboy
2025-07-14 11:05:06 +08:00
parent 4e59a71c7d
commit 2c1d9139da
15 changed files with 220 additions and 53 deletions
@@ -939,9 +939,9 @@ internal partial class DataGridColumnHeader : ContentControl
{
if (IsMotionEnabled)
{
if (Transitions != null)
if (_frame != null)
{
Transitions = new Transitions()
_frame.Transitions = new Transitions()
{
TransitionUtils.CreateTransition<SolidColorBrushTransition>(BackgroundProperty)
};
@@ -949,8 +949,11 @@ internal partial class DataGridColumnHeader : ContentControl
}
else
{
Transitions?.Clear();
Transitions = null;
if (_frame != null)
{
_frame.Transitions?.Clear();
_frame.Transitions = null;
}
}
}
}
@@ -147,21 +147,28 @@
<!-- BuildSortableColumnStyle 开始 -->
<Style Selector="^[CanUserSort=True]">
<Style Selector="^:sort-ascending, ^:sort-descending">
<Setter Property="Background"
Value="{DynamicResource {x:Static atom:DataGridTokenKey.HeaderSortActiveBg}}" />
<Style Selector="^ /template/ Border#PART_Frame">
<Setter Property="Background"
Value="{DynamicResource {x:Static atom:DataGridTokenKey.HeaderSortActiveBg}}" />
</Style>
</Style>
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^:pointerover">
<Setter Property="Background"
Value="{DynamicResource {x:Static atom:DataGridTokenKey.HeaderSortHoverBg}}" />
<Style Selector="^ /template/ Border#PART_Frame">
<Setter Property="Background"
Value="{DynamicResource {x:Static atom:DataGridTokenKey.HeaderSortHoverBg}}" />
</Style>
</Style>
</Style>
<!-- BuildSortableColumnStyle 结束 -->
<Style Selector="^ /template/ Border#PART_Frame">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="^[IsShowFrozenShadow=True]">
<Style Selector="^ /template/ Border#PART_Frame">
<Setter Property="Background" Value="{DynamicResource {x:Static atom:DataGridTokenKey.TableHeaderBg}}" />
</Style>
@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
namespace AtomUI.Controls;
@@ -226,6 +227,11 @@ public class ArrowDecoratedBox : ContentControl,
return CornerRadius;
}
public IBrush? GetMaskBackground()
{
return Background;
}
public Rect GetMaskBounds()
{
Debug.Assert(_arrowIndicatorLayout != null && _contentDecorator != null);
@@ -8,6 +8,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace AtomUI.Controls;
@@ -215,6 +216,11 @@ public class MenuFlyoutPresenter : MenuBase,
return Bounds;
}
public IBrush? GetMaskBackground()
{
return _arrowDecoratedBox?.Background;
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
namespace AtomUI.Controls;
@@ -157,6 +158,11 @@ public class TreeViewFlyoutPresenter : FloatableTreeView,
return Bounds;
}
public IBrush? GetMaskBackground()
{
return _arrowDecoratedBox?.Background;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
@@ -6,6 +7,7 @@ internal interface IShadowMaskInfoProvider
{
CornerRadius GetMaskCornerRadius();
Rect GetMaskBounds();
IBrush? GetMaskBackground();
}
internal interface IArrowAwareShadowMaskInfoProvider : IShadowMaskInfoProvider
+2 -2
View File
@@ -564,7 +564,7 @@ public class Popup : AvaloniaPopup,
public void MotionAwareClose(Action? closed = null)
{
Debug.Assert(_buddyLayer != null);
if (_closeAnimating)
{
return;
@@ -580,7 +580,7 @@ public class Popup : AvaloniaPopup,
{
return;
}
Debug.Assert(_buddyLayer != null);
if (!IsMotionEnabled)
{
_buddyLayer?.Detach();
@@ -6,8 +6,10 @@ using AtomUI.MotionScene;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Threading;
namespace AtomUI.Controls;
@@ -60,17 +62,25 @@ internal class PopupBuddyDecorator : SceneMotionActorControl
{
if (_popupHost is PopupRoot oldPopupRoot)
{
oldPopupRoot.SizeChanged -= HandleBuddyPopupRootSizeChanged;
var oldPresenter = oldPopupRoot.Presenter;
if (oldPresenter != null)
{
oldPresenter.SizeChanged -= HandleBuddyPopupRootSizeChanged;
}
}
popupRoot.SizeChanged += HandleBuddyPopupRootSizeChanged;
ConfigureDecorator(popupRoot);
var presenter = popupRoot.Presenter;
if (presenter != null)
{
presenter.SizeChanged += HandleBuddyPopupRootSizeChanged;
}
ConfigureDecorator(presenter);
CaptureContentControl();
}
_popupHost = popupHost;
}
private void ConfigureDecorator(IPopupHost popupHost)
private void ConfigureDecorator(ContentPresenter? presenter)
{
var presenter = popupHost.Presenter;
if (presenter != null)
{
var content = presenter.Child;
@@ -91,15 +101,15 @@ internal class PopupBuddyDecorator : SceneMotionActorControl
_decoratorControl.MaskCornerRadius = templatedControl.CornerRadius;
_decoratorControl.MaskSize = templatedControl.Bounds.Size;
}
CaptureContentControl();
}
}
private void HandleBuddyPopupRootSizeChanged(object? sender, SizeChangedEventArgs e)
{
if (sender is PopupRoot popupRoot)
if (sender is ContentPresenter presenter)
{
ConfigureDecorator(popupRoot);
ConfigureDecorator(presenter);
ConfigureBlankContentControl();
}
}
@@ -125,19 +135,59 @@ internal class PopupBuddyDecorator : SceneMotionActorControl
}
}
internal void HideDecoratorContent()
internal void ConfigureBlankContentControl()
{
if (_decoratorControl.Content != null)
if (_popupHost != null && _popupHost.Presenter != null)
{
_decoratorControl.Content.Opacity = 0.0;
}
}
var content = _popupHost.Presenter.Child;
if (content != null)
{
if (content is IShadowMaskInfoProvider shadowMaskInfoProvider)
{
var maskBounds = shadowMaskInfoProvider.GetMaskBounds();
var offset = maskBounds.Position;
internal void ShowDecoratorContent()
{
if (_decoratorControl.Content != null)
{
_decoratorControl.Content.Opacity = 1.0;
var contentWidth = maskBounds.Width;
var contentHeight = maskBounds.Height;
var blankControl = new Canvas()
{
Background = Brushes.Transparent,
Width = contentWidth + offset.X,
Height = contentHeight + offset.Y,
};
var blankContent = new Border()
{
Background = shadowMaskInfoProvider.GetMaskBackground(),
CornerRadius = shadowMaskInfoProvider.GetMaskCornerRadius(),
Width = contentWidth,
Height = contentHeight,
};
blankControl.Children.Add(blankContent);
Canvas.SetLeft(blankContent, offset.X);
Canvas.SetTop(blankContent, offset.Y);
_decoratorControl.Content = blankControl;
}
else if (content is Border bordered)
{
_decoratorControl.Content = new Border
{
Background = bordered.Background,
CornerRadius = bordered.CornerRadius,
Width = bordered.Bounds.Size.Width,
Height = bordered.Bounds.Size.Height
};
}
else if (content is TemplatedControl templatedControl)
{
_decoratorControl.Content = new Border
{
Background = templatedControl.Background,
CornerRadius = templatedControl.CornerRadius,
Width = templatedControl.Bounds.Size.Width,
Height = templatedControl.Bounds.Size.Height
};
}
}
}
}
}
+1 -1
View File
@@ -184,7 +184,6 @@ internal class PopupBuddyLayer : SceneLayer, IPopupBuddyLayer, IShadowAwareLayer
}
SetupPopupHost();
_buddyDecorator.HideDecoratorContent();
}
public void Detach()
@@ -214,6 +213,7 @@ internal class PopupBuddyLayer : SceneLayer, IPopupBuddyLayer, IShadowAwareLayer
motion.Run(_buddyDecorator, null, () =>
{
completedAction?.Invoke();
_buddyDecorator.ConfigureBlankContentControl();
NotifyAttachMotionCompleted();
});
}
+54 -13
View File
@@ -1,6 +1,7 @@
using System.Globalization;
using AtomUI.Controls.Themes;
using AtomUI.Input;
using AtomUI.Media;
using AtomUI.Theme;
using AtomUI.Theme.Utils;
using AtomUI.Utils;
@@ -18,6 +19,7 @@ using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
@@ -129,7 +131,7 @@ public class Slider : RangeBase,
SliderTrack.MarksProperty.AddOwner<Slider>();
public static readonly StyledProperty<string> ValueFormatTemplateProperty =
AvaloniaProperty.Register<Slider, string>(nameof(ValueFormatTemplate), "{0:0.00}");
AvaloniaProperty.Register<Slider, string>(nameof(ValueFormatTemplate), "{0:0}");
public static readonly StyledProperty<bool> IncludedProperty =
SliderTrack.IncludedProperty.AddOwner<Slider>();
@@ -232,6 +234,16 @@ public class Slider : RangeBase,
string IControlSharedTokenResourcesHost.TokenId => SliderToken.ID;
#endregion
/// <summary>
/// Gets a value indicating whether the <see cref="Slider" /> is currently being dragged.
/// </summary>
protected bool IsDragging { get; private set; }
/// <summary>
/// Gets the <see cref="SliderTrack" /> part of the <see cref="Slider" />.
/// </summary>
protected SliderTrack? SliderTrack { get; private set; }
// Slider required parts
private bool _isFocusEngaged;
@@ -239,6 +251,7 @@ public class Slider : RangeBase,
private IDisposable? _pointerMovedDispose;
private IDisposable? _pointerPressDispose;
private IDisposable? _pointerReleaseDispose;
private double _tipHostWidth;
private const double Tolerance = 0.0001;
@@ -259,16 +272,6 @@ public class Slider : RangeBase,
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Slider>(AutomationControlType.Slider);
}
/// <summary>
/// Gets a value indicating whether the <see cref="Slider" /> is currently being dragged.
/// </summary>
protected bool IsDragging { get; private set; }
/// <summary>
/// Gets the <see cref="SliderTrack" /> part of the <see cref="Slider" />.
/// </summary>
protected SliderTrack? SliderTrack { get; private set; }
/// <summary>
/// Instantiates a new instance of the <see cref="Slider" /> class.
/// </summary>
@@ -298,6 +301,7 @@ public class Slider : RangeBase,
_pointerReleaseDispose =
this.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
_pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
ConfigureTipHostWidth();
if (SliderTrack is not null)
{
@@ -321,12 +325,11 @@ public class Slider : RangeBase,
}
}
}
SetupSliderThumbPlacement();
}
// TODO 在 rangemode 下可能没有用
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
@@ -674,6 +677,15 @@ public class Slider : RangeBase,
}
}
}
if (this.IsAttachedToVisualTree())
{
if (change.Property == MaximumProperty ||
change.Property == ValueFormatTemplateProperty)
{
ConfigureTipHostWidth();
}
}
}
private string FormatValue(double value)
@@ -728,4 +740,33 @@ public class Slider : RangeBase,
PseudoClasses.Set(StdPseudoClass.Vertical, o == Orientation.Vertical);
PseudoClasses.Set(StdPseudoClass.Horizontal, o == Orientation.Horizontal);
}
private void ConfigureTipHostWidth()
{
var maxValueText = FormatValue(Maximum);
var size = TextUtils.CalculateTextSize(maxValueText, FontSize, FontFamily);
_tipHostWidth = size.Width * 1.1;
if (SliderTrack is not null)
{
if (!IsRangeMode)
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTipHostWidth(SliderTrack.StartSliderThumb, _tipHostWidth);
}
}
else
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTipHostWidth(SliderTrack.StartSliderThumb, _tipHostWidth);
}
if (SliderTrack.EndSliderThumb is not null)
{
ToolTip.SetTipHostWidth(SliderTrack.EndSliderThumb, _tipHostWidth);
}
}
}
}
}
@@ -14,7 +14,15 @@ internal class TabControlOverflowMenuItem : BaseOverflowMenuItem
{
var eventArgs = new CloseTabRequestEventArgs(CloseTabEvent, TabItem!);
RaiseEvent(eventArgs);
Dispatcher.UIThread.Post(() => { menu.Close(); });
if (menu is MenuFlyoutPresenter menuFlyoutPresenter)
{
var menuFlyout = menuFlyoutPresenter.MenuFlyout;
menuFlyout?.Items.Remove(this);
if (menuFlyout?.Items.Count == 0)
{
Dispatcher.UIThread.Post(() => { menu.Close(); });
}
}
}
}
}
@@ -14,7 +14,15 @@ internal class TabStripOverflowMenuItem : BaseOverflowMenuItem
{
var eventArgs = new CloseTabRequestEventArgs(CloseTabEvent, TabStripItem!);
RaiseEvent(eventArgs);
Dispatcher.UIThread.Post(() => { menu.Close(); });
if (menu is MenuFlyoutPresenter menuFlyoutPresenter)
{
var menuFlyout = menuFlyoutPresenter.MenuFlyout;
menuFlyout?.Items.Remove(this);
if (menuFlyout?.Items.Count == 0)
{
Dispatcher.UIThread.Post(() => { menu.Close(); });
}
}
}
}
}
@@ -2,20 +2,23 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:atom="https://atomui.net">
<atom:StringToTextBlockConverter x:Key="StringToTextBlockConverter" VerticalAlignment="Center" />
<atom:StringToTextBlockConverter x:Key="StringToTextBlockConverter" VerticalAlignment="Center" HorizontalAlignment="Center" />
<ControlTheme x:Key="{x:Type atom:ToolTip}" TargetType="atom:ToolTip">
<Setter Property="Template">
<ControlTemplate>
<atom:ArrowDecoratedBox Name="{x:Static atom:ToolTipThemeConstants.ToolTipContainerPart}"
Content="{TemplateBinding Content, Converter={StaticResource StringToTextBlockConverter}}"
ContentTemplate="{TemplateBinding ContentTemplate}"
/>
Background="{TemplateBinding Background}">
<ContentPresenter
Name="{x:Static atom:ToolTipThemeConstants.ToolTipContainerPresenterPart}"
Content="{TemplateBinding Content, Converter={StaticResource StringToTextBlockConverter}}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</atom:ArrowDecoratedBox>
</ControlTemplate>
</Setter>
<Setter Property="Background" Value="{DynamicResource {x:Static atom:ToolTipTokenKey.ToolTipBackground}}" />
<Style Selector="^ /template/ atom|ArrowDecoratedBox#PART_ToolTipContainer">
<Setter Property="FontSize" Value="{DynamicResource {x:Static atom:SharedTokenKey.FontSize}}" />
<Setter Property="MaxWidth" Value="{DynamicResource {x:Static atom:ToolTipTokenKey.ToolTipMaxWidth}}" />
<Setter Property="Background" Value="{DynamicResource {x:Static atom:ToolTipTokenKey.ToolTipBackground}}" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static atom:ToolTipTokenKey.ToolTipColor}}" />
<Setter Property="MinHeight" Value="{DynamicResource {x:Static atom:SharedTokenKey.ControlHeight}}" />
<Setter Property="Padding" Value="{DynamicResource {x:Static atom:ToolTipTokenKey.ToolTipPadding}}" />
@@ -3,4 +3,5 @@ namespace AtomUI.Controls.Themes;
internal class ToolTipThemeConstants
{
public const string ToolTipContainerPart = "PART_ToolTipContainer";
public const string ToolTipContainerPresenterPart = "PART_ToolTipContentPresenter";
}
+28 -2
View File
@@ -7,6 +7,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
@@ -26,6 +27,9 @@ public class ToolTip : ContentControl,
public static readonly AttachedProperty<object?> TipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, object?>("Tip");
public static readonly AttachedProperty<double> TipHostWidthProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, double>("TipHostWidth", double.NaN);
public static readonly AttachedProperty<bool> IsOpenProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, bool>("IsOpen");
@@ -109,6 +113,16 @@ public class ToolTip : ContentControl,
element.SetValue(TipProperty, value);
}
public static double GetTipHostWidth(Control element)
{
return element.GetValue(TipHostWidthProperty);
}
public static void SetTipHostWidth(Control element, double value)
{
element.SetValue(TipHostWidthProperty, value);
}
public static bool GetIsOpen(Control element)
{
return element.GetValue(IsOpenProperty);
@@ -316,6 +330,7 @@ public class ToolTip : ContentControl,
private Popup? _popup;
private Action<IPopupHost?>? _popupHostChangedHandler;
private ArrowDecoratedBox? _arrowDecoratedBox;
private ContentPresenter? _contentPresenter;
private DispatcherTimer? _timer;
static ToolTip()
@@ -340,6 +355,11 @@ public class ToolTip : ContentControl,
return _arrowDecoratedBox.GetMaskBounds();
}
public IBrush? GetMaskBackground()
{
return Background;
}
private void Open(Control control)
{
StopTimer();
@@ -407,6 +427,11 @@ public class ToolTip : ContentControl,
if (_arrowDecoratedBox is not null)
{
SetToolTipColor(control);
if (_contentPresenter != null)
{
_contentPresenter.Width = GetTipHostWidth(control);
}
_arrowDecoratedBox.Bind(ArrowDecoratedBox.IsShowArrowProperty,
control.GetBindingObservable(IsShowArrowProperty, flag =>
{
@@ -475,12 +500,12 @@ public class ToolTip : ContentControl,
if (presetColorType is not null)
{
var presetColor = PresetPrimaryColor.GetColor(presetColorType.Value);
_arrowDecoratedBox.Background = new SolidColorBrush(presetColor.Color());
Background = new SolidColorBrush(presetColor.Color());
InvalidateVisual();
}
else if (color is not null)
{
_arrowDecoratedBox.Background = new SolidColorBrush(color.Value);
Background = new SolidColorBrush(color.Value);
InvalidateVisual();
}
}
@@ -601,6 +626,7 @@ public class ToolTip : ContentControl,
{
base.OnApplyTemplate(e);
_arrowDecoratedBox = e.NameScope.Find<ArrowDecoratedBox>(ToolTipThemeConstants.ToolTipContainerPart);
_contentPresenter = e.NameScope.Find<ContentPresenter>(ToolTipThemeConstants.ToolTipContainerPresenterPart);
}
private Point CalculatePopupPositionDelta(Control control,