Refactor Tooltip

This commit is contained in:
chinboy
2025-02-27 14:49:40 +08:00
parent 7fa36bbc5d
commit f890e4c614
19 changed files with 679 additions and 858 deletions
@@ -9,8 +9,6 @@ namespace AtomUI.MotionScene;
public class AbstractMotion : IMotion
{
public bool IsRunning { get; }
public RelativePoint RenderTransformOrigin { get; protected set; }
public IList<Animation> Animations { get; }
public TimeSpan Duration { get; }
@@ -41,12 +39,13 @@ public class AbstractMotion : IMotion
{
aboutToStart();
}
actor.IsVisible = true;
foreach (var animation in Animations)
{
await animation.RunAsync(actor, cancellationToken);
}
actor.NotifyMotionCompleted();
NotifyCompleted();
if (completedAction is not null)
-1
View File
@@ -6,7 +6,6 @@ namespace AtomUI.MotionScene;
internal interface IMotion
{
public bool IsRunning { get; }
public RelativePoint RenderTransformOrigin { get; }
public IList<Animation> Animations { get; }
public TimeSpan Duration { get; }
+15 -15
View File
@@ -16,21 +16,21 @@ internal static class MotionInvoker
await motion.RunAsync(actor, aboutToStart, completedAction, cancellationToken);
});
}
public static async Task InvokeAsync(MotionActorControl actor,
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
{
await motion.RunAsync(actor, aboutToStart, completedAction, cancellationToken);
}
public static void InvokeInPopupLayer(SceneMotionActorControl actor,
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
{
Dispatcher.UIThread.Invoke(async () =>
{
@@ -39,12 +39,12 @@ internal static class MotionInvoker
}
public static async Task InvokeInPopupLayerAsync(SceneMotionActorControl actor,
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
AbstractMotion motion,
Action? aboutToStart = null,
Action? completedAction = null,
CancellationToken cancellationToken = default)
{
var sceneLayer = PrepareSceneLayer(motion, actor);
var sceneLayer = PrepareSceneLayer(motion, actor);
var compositeDisposable = new CompositeDisposable();
compositeDisposable.Add(Disposable.Create(sceneLayer, (state) =>
{
@@ -53,6 +53,16 @@ public static class ObjectExtension
return false;
}
public static T? GetPropertyOrThrow<T>(this object source, PropertyInfo info)
{
var obj = info.GetValue(source);
if (obj is T propertyOrThrow)
{
return propertyOrThrow;
}
throw new Exception($"Get Property {info.Name} value failed.");
}
public static T? GetPropertyOrThrow<T>(this object source, string name,
BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic |
BindingFlags.Public | BindingFlags.FlattenHierarchy)
@@ -0,0 +1,14 @@
using AtomUI.Reflection;
using Avalonia;
namespace AtomUI.Utils;
// 反射扩展类
internal static class AvaloniaPropertyReflectionExtensions
{
public static void NotifyChanged<TValue>(this AvaloniaProperty<TValue> property, AvaloniaPropertyChangedEventArgs<TValue> e)
{
var methodInfo = typeof(AvaloniaProperty<TValue>).GetMethodInfoOrThrow("NotifyChanged");
methodInfo.Invoke(property, [e]);
}
}
@@ -1,6 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using AtomUI.Theme;
using AtomUI.Theme.Utils;
using Avalonia;
@@ -176,6 +174,7 @@ public class ArrowDecoratedBox : ContentControl,
{
AffectsMeasure<ArrowDecoratedBox>(IsShowArrowProperty, ArrowDirectionProperty);
AffectsArrange<ArrowDecoratedBox>(ArrowPositionProperty);
AffectsRender<ArrowDecoratedBox>(BackgroundProperty);
}
public ArrowDecoratedBox()
@@ -261,7 +260,7 @@ public class ArrowDecoratedBox : ContentControl,
}
return targetRect;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
@@ -426,5 +425,4 @@ public class ArrowDecoratedBox : ContentControl,
_arrowPlacementFlipped = false;
}
}
+9 -6
View File
@@ -154,6 +154,7 @@ public class Flyout : PopupFlyoutBase
var anchor = popup.PlacementAnchor;
var gravity = popup.PlacementGravity;
// TODO 可以改进成绑定,可以参考 Tooltip 的做法
if (flyoutPresenter is not null)
{
var arrowPosition = PopupUtils.CalculateArrowPosition(placement, anchor, gravity);
@@ -345,12 +346,10 @@ public class Flyout : PopupFlyoutBase
}
IsOpen = true;
Dispatcher.UIThread.InvokeAsync(() =>
Dispatcher.UIThread.InvokeAsync(async () =>
{
Popup.OpenAnimation(() =>
{
HandlePopupOpened(placementTarget);
});
await Popup.OpenAsync();
HandlePopupOpened(placementTarget);
});
return true;
}
@@ -376,7 +375,11 @@ public class Flyout : PopupFlyoutBase
}
IsOpen = false;
Dispatcher.UIThread.InvokeAsync(() => { Popup.CloseAnimation(HandlePopupClosed); });
Dispatcher.UIThread.InvokeAsync(async () =>
{
await Popup.CloseAsync();
HandlePopupClosed();
});
return true;
}
+43 -41
View File
@@ -19,6 +19,7 @@ namespace AtomUI.Controls;
public class FlyoutPresenterCreatedEventArgs : EventArgs
{
public Control Presenter { get; }
public FlyoutPresenterCreatedEventArgs(Control presenter)
{
Presenter = presenter;
@@ -33,60 +34,60 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
#region 公共属性定义
/// <summary>
/// 距离 anchor 的边距,根据垂直和水平进行设置
/// 但是对某些组合无效,比如跟随鼠标的情况
/// 还有些 anchor 和 gravity 的组合也没有用
/// </summary>
public static readonly StyledProperty<double> MarginToAnchorProperty =
/// 距离 anchor 的边距,根据垂直和水平进行设置
/// 但是对某些组合无效,比如跟随鼠标的情况
/// 还有些 anchor 和 gravity 的组合也没有用
/// </summary>
public static readonly StyledProperty<double> MarginToAnchorProperty =
Popup.MarginToAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementProperty" />
public static readonly StyledProperty<PlacementMode> PlacementProperty =
/// <inheritdoc cref="Popup.PlacementProperty" />
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Avalonia.Controls.Primitives.Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty" />
public static readonly StyledProperty<double> HorizontalOffsetProperty =
/// <inheritdoc cref="Popup.HorizontalOffsetProperty" />
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Avalonia.Controls.Primitives.Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.VerticalOffsetProperty" />
public static readonly StyledProperty<double> VerticalOffsetProperty =
/// <inheritdoc cref="Popup.VerticalOffsetProperty" />
public static readonly StyledProperty<double> VerticalOffsetProperty =
Avalonia.Controls.Primitives.Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty" />
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
/// <inheritdoc cref="Popup.PlacementAnchorProperty" />
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Avalonia.Controls.Primitives.Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty" />
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
/// <inheritdoc cref="Popup.PlacementAnchorProperty" />
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Avalonia.Controls.Primitives.Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="ShowMode" /> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
/// <summary>
/// Defines the <see cref="ShowMode" /> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement" /> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement" /> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Avalonia.Controls.Primitives.Popup.OverlayInputPassThroughElementProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment" /> property
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment" /> property
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Avalonia.Controls.Primitives.Popup.PlacementConstraintAdjustmentProperty.AddOwner<PopupFlyoutBase>();
public static readonly StyledProperty<bool> IsMotionEnabledProperty
= AvaloniaProperty.Register<PopupFlyoutBase, bool>(nameof(IsMotionEnabled));
public static readonly StyledProperty<bool> IsMotionEnabledProperty
= AvaloniaProperty.Register<PopupFlyoutBase, bool>(nameof(IsMotionEnabled));
public double MarginToAnchor
{
get => GetValue(MarginToAnchorProperty);
set => SetValue(MarginToAnchorProperty, value);
}
/// <inheritdoc cref="Popup.Placement" />
public PlacementMode Placement
{
@@ -147,7 +148,7 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
@@ -160,7 +161,7 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
internal static readonly StyledProperty<bool> IsDetectMouseClickEnabledProperty =
AvaloniaProperty.Register<Flyout, bool>(nameof(IsDetectMouseClickEnabled), true);
internal bool IsDetectMouseClickEnabled
{
get => GetValue(IsDetectMouseClickEnabledProperty);
@@ -284,6 +285,7 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
return false;
}
}
IsOpen = false;
Popup.IsOpen = false;
@@ -304,8 +306,8 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
Target.DetachedFromVisualTree -= HandlePlacementTargetDetachedFromVisualTree;
Target.KeyUp -= HandlePlacementTargetOrPopupKeyUp;
}
OnClosed();
@@ -377,8 +379,8 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
{
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
placementTarget.DetachedFromVisualTree += HandlePlacementTargetDetachedFromVisualTree;
placementTarget.KeyUp += HandlePlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
@@ -400,7 +402,7 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
}
}
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
private void HandlePlacementTargetDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
@@ -494,9 +496,9 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
// popup.Closing += OnPopupClosing;
var handler = new EventHandler<CancelEventArgs>(OnPopupClosing);
var closingEventAddMethod = ClosingEventInfo.GetAddMethod(true);
closingEventAddMethod?.Invoke(popup, new object?[] { handler });
closingEventAddMethod?.Invoke(popup, [handler]);
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
popup.KeyUp += HandlePlacementTargetOrPopupKeyUp;
NotifyPopupCreated(popup);
return popup;
}
@@ -524,7 +526,7 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
}
// This method is handling both popup logical tree and target logical tree.
private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
private void HandlePlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
{
if (!e.Handled
&& IsOpen
@@ -708,7 +708,6 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey ToolTipPadding = new TokenResourceKey("ToolTip.ToolTipPadding");
public static readonly TokenResourceKey ToolTipShadows = new TokenResourceKey("ToolTip.ToolTipShadows");
public static readonly TokenResourceKey ToolTipMotionDuration = new TokenResourceKey("ToolTip.ToolTipMotionDuration");
public static readonly TokenResourceKey MarginToAnchor = new TokenResourceKey("ToolTip.MarginToAnchor");
public static readonly TokenResourceKey ToolTipArrowSize = new TokenResourceKey("ToolTip.ToolTipArrowSize");
}
+39 -35
View File
@@ -3,7 +3,6 @@ using AtomUI.Controls.Primitives;
using AtomUI.Data;
using AtomUI.Media;
using AtomUI.MotionScene;
using AtomUI.Reflection;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Theme.Utils;
@@ -109,7 +108,7 @@ public class Popup : AvaloniaPopup,
private IDisposable? _selfLightDismissDisposable;
private IManagedPopupPositionerPopup? _managedPopupPositionerX;
private IManagedPopupPositionerPopup? _managedPopupPositioner;
private bool _isNeedDetectFlip = true;
// 在翻转之后或者恢复正常,会有属性的变动,在变动之后捕捉动画需要等一个事件循环,保证布局已经生效
private bool _isNeedWaitFlipSync = false;
@@ -160,8 +159,9 @@ public class Popup : AvaloniaPopup,
HorizontalOffset = offsetX;
VerticalOffset = offsetY;
_selfLightDismissDisposable?.Dispose();
_shadowLayer?.Close();
_firstDetected = true;
}
@@ -172,11 +172,7 @@ public class Popup : AvaloniaPopup,
var popupPositioner = popupRoot.PlatformImpl?.PopupPositioner;
if (popupPositioner is ManagedPopupPositioner managedPopupPositioner)
{
if (managedPopupPositioner.TryGetField("_popup",
out IManagedPopupPositionerPopup? managedPopupPositionerPopup))
{
_managedPopupPositionerX = managedPopupPositionerPopup;
}
_managedPopupPositioner = managedPopupPositioner.GetManagedPopupPositionerPopup();
}
}
@@ -230,7 +226,10 @@ public class Popup : AvaloniaPopup,
{
if (popupHostProvider.PopupHost != pointerEventArgs.Root)
{
CloseAnimation();
Dispatcher.UIThread.InvokeAsync(async () =>
{
await CloseAsync();
});
}
}
}
@@ -303,13 +302,13 @@ public class Popup : AvaloniaPopup,
private Rect GetBounds(Rect anchorRect)
{
// 暂时只支持窗口的方式
if (_managedPopupPositionerX is null)
if (_managedPopupPositioner is null)
{
throw new InvalidOperationException("ManagedPopupPositioner is null");
}
var parentGeometry = _managedPopupPositionerX.ParentClientAreaScreenGeometry;
var screens = _managedPopupPositionerX.Screens;
var parentGeometry = _managedPopupPositioner.ParentClientAreaScreenGeometry;
var screens = _managedPopupPositioner.Screens;
return GetBounds(anchorRect, parentGeometry, screens);
}
@@ -376,12 +375,12 @@ public class Popup : AvaloniaPopup,
popupSize = Child.DesiredSize;
}
Debug.Assert(_managedPopupPositionerX != null);
var scaling = _managedPopupPositionerX.Scaling;
Debug.Assert(_managedPopupPositioner != null);
var scaling = _managedPopupPositioner.Scaling;
var anchorRect = new Rect(
parameters.AnchorRectangle.TopLeft * scaling,
parameters.AnchorRectangle.Size * scaling);
anchorRect = anchorRect.Translate(_managedPopupPositionerX.ParentClientAreaScreenGeometry.TopLeft);
anchorRect = anchorRect.Translate(_managedPopupPositioner.ParentClientAreaScreenGeometry.TopLeft);
var flipInfo = CalculateFlipInfo(popupSize * scaling,
anchorRect,
@@ -465,23 +464,24 @@ public class Popup : AvaloniaPopup,
};
}
// TODO Review 需要评估这里等待的变成模式是否正确高效
public void OpenAnimation(Action? opened = null)
public async Task OpenAsync()
{
// AbstractPopup is currently open
if (IsOpen || _openAnimating || _closeAnimating)
{
return;
await Task.CompletedTask;
}
if (!IsMotionEnabled)
{
Open();
opened?.Invoke();
return;
await Task.CompletedTask;
}
_openAnimating = true;
var tsc = new TaskCompletionSource();
Open();
var popupRoot = Host as PopupRoot;
@@ -492,14 +492,17 @@ public class Popup : AvaloniaPopup,
Dispatcher.UIThread.Post(() =>
{
popupRoot.Opacity = 1.0;
RunOpenAnimation(popupRoot, opened);
RunOpenAnimation(popupRoot, tsc);
});
return;
}
RunOpenAnimation(popupRoot, opened);
else
{
RunOpenAnimation(popupRoot, tsc);
}
await tsc.Task;
}
private void RunOpenAnimation(PopupRoot popupRoot, Action? opened = null)
private void RunOpenAnimation(PopupRoot popupRoot, TaskCompletionSource tsc)
{
var placementTarget = GetEffectivePlacementTarget();
var popupOffset = popupRoot.PlatformImpl!.Position;
@@ -533,34 +536,34 @@ public class Popup : AvaloniaPopup,
{
_shadowLayer?.Open();
popupRoot.Show();
opened?.Invoke();
_isNeedWaitFlipSync = false;
_openAnimating = false;
if (RequestCloseWhereAnimationCompleted)
{
RequestCloseWhereAnimationCompleted = false;
Dispatcher.UIThread.InvokeAsync(() => { CloseAnimation(); });
Dispatcher.UIThread.InvokeAsync(async () => { await CloseAsync(); });
}
tsc.SetResult();
});
});
}
public void CloseAnimation(Action? closed = null)
public async Task CloseAsync()
{
if (_closeAnimating)
{
return;
await Task.CompletedTask;
}
if (_openAnimating)
{
RequestCloseWhereAnimationCompleted = true;
return;
await Task.CompletedTask;
}
if (!IsOpen)
{
return;
await Task.CompletedTask;
}
if (!IsMotionEnabled)
@@ -568,11 +571,11 @@ public class Popup : AvaloniaPopup,
_shadowLayer?.Close();
_isNeedDetectFlip = true;
Close();
closed?.Invoke();
return;
await Task.CompletedTask;
}
_closeAnimating = true;
var tsc = new TaskCompletionSource();
var motion = new ZoomBigOutMotion(MotionDuration);
@@ -602,16 +605,17 @@ public class Popup : AvaloniaPopup,
{
MotionInvoker.InvokeInPopupLayer(motionActor, motion, () =>
{
popupRoot.Hide();
_shadowLayer?.Close();
popupRoot.Hide();
}, () =>
{
_closeAnimating = false;
_isNeedDetectFlip = true;
Close();
closed?.Invoke();
tsc.SetResult();
});
});
await tsc.Task;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -1,4 +1,7 @@
using System.Reflection;
using AtomUI.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
@@ -6,6 +9,12 @@ namespace AtomUI.Controls;
internal static class PopupReflectionExtensions
{
#region 反射信息定义
private static readonly Lazy<MethodInfo> SetPopupParentMethodInfo = new Lazy<MethodInfo>(
typeof(Popup).GetMethodInfoOrThrow("SetPopupParent",
BindingFlags.Instance | BindingFlags.NonPublic));
#endregion
public static void MoveAndResize(this Popup popup, Point devicePoint, Size virtualSize)
{
if (popup.Host is PopupRoot popupRoot)
@@ -17,4 +26,9 @@ internal static class PopupReflectionExtensions
}
}
}
public static void SetPopupParent(this Popup popup, Control? newParent)
{
SetPopupParentMethodInfo.Value.Invoke(popup, [newParent]);
}
}
+33 -35
View File
@@ -53,12 +53,32 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
private Popup _target;
private ShadowLayerPopupOpenState? _openState;
private ShadowRenderer? _shadowRenderer;
private bool _isOpenRequested;
private IManagedPopupPositionerPopup? _managedPopupPositionerPopup;
public PopupShadowLayer(Popup target)
{
_target = target;
// if (target is IPopupHostProvider popupHostProvider)
// {
// popupHostProvider.PopupHostChanged += host =>
// {
// if (host is PopupRoot popupRoot)
// {
// popupRoot.PositionChanged += (sender, args) =>
// {
// Console.WriteLine(args.Point);
// };
// }
// };
// }
// if (_target.Host is PopupRoot targetPopupRoot)
// {
// targetPopupRoot.PositionChanged += (sender, args) =>
// {
// Console.WriteLine(args.Point);
// };
// }
}
private void HandleTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@@ -133,19 +153,6 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
return new Size(targetWidth, targetHeight);
}
private void HandleTargetOpened(object? sender, EventArgs args)
{
if (_isOpenRequested)
{
Open();
}
}
private void HandleTargetClosed(object? sender, EventArgs args)
{
Close();
}
protected virtual void NotifyPopupHostCreated(IPopupHost popupHost)
{
}
@@ -157,15 +164,7 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
{
return;
}
if (_target.Host == null)
{
_isOpenRequested = true;
return;
}
_isOpenRequested = false;
var popupHost = OverlayPopupHost.CreatePopupHost(_target.PlacementTarget!, DependencyResolver.Instance);
popupHost.Topmost = false;
NotifyPopupHostCreated(popupHost);
@@ -213,7 +212,7 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
if (targetPopupRoot != null)
{
SubscribeToEventHandler<PopupRoot, EventHandler<PixelPointEventArgs>>(targetPopupRoot,
ParentPopupPositionChanged,
HandleParentPopupPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
}
@@ -222,12 +221,6 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
HandleTargetPropertyChanged,
(x, handler) => x.PropertyChanged += handler,
(x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<Popup, EventHandler>(_target, HandleTargetOpened,
(x, handler) => x.Opened += handler,
(x, handler) => x.Opened -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(_target, HandleTargetClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
@@ -248,7 +241,10 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
{
Dispatcher.UIThread.Post(() =>
{
_target.Host.Topmost = true;
if (_target.Host is not null)
{
_target.Host.Topmost = true;
}
targetPopupRoot.Activate();
});
}
@@ -256,7 +252,6 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
public virtual void Close()
{
_isOpenRequested = false;
if (_openState is null)
{
return;
@@ -289,11 +284,14 @@ internal class PopupShadowLayer : AvaloniaObject, IShadowDecorator
}
}
private void ParentPopupPositionChanged(object? src, PixelPointEventArgs e) => HandlePositionChange();
private void HandlePositionChange()
private void HandleParentPopupPositionChanged(object? src, PixelPointEventArgs e)
{
if (src is PopupRoot targetPopupRoot)
{
UpdateLayoutHostPositionAndSize(targetPopupRoot);
}
}
private void RootTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
+10 -10
View File
@@ -302,19 +302,19 @@ public class Slider : RangeBase,
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(Value));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(Value));
}
}
else
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(RangeValue.StartValue));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(RangeValue.StartValue));
}
if (SliderTrack.EndSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.EndSliderThumb, FormatValue(RangeValue.EndValue));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.EndSliderThumb, FormatValue(RangeValue.EndValue));
}
}
}
@@ -616,24 +616,24 @@ public class Slider : RangeBase,
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetPlacement(SliderTrack.StartSliderThumb, PlacementMode.Top);
Avalonia.Controls.ToolTip.SetPlacement(SliderTrack.StartSliderThumb, PlacementMode.Top);
}
if (SliderTrack.EndSliderThumb is not null)
{
ToolTip.SetPlacement(SliderTrack.EndSliderThumb, PlacementMode.Top);
Avalonia.Controls.ToolTip.SetPlacement(SliderTrack.EndSliderThumb, PlacementMode.Top);
}
}
else
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetPlacement(SliderTrack.StartSliderThumb, PlacementMode.Right);
Avalonia.Controls.ToolTip.SetPlacement(SliderTrack.StartSliderThumb, PlacementMode.Right);
}
if (SliderTrack.EndSliderThumb is not null)
{
ToolTip.SetPlacement(SliderTrack.EndSliderThumb, PlacementMode.Right);
Avalonia.Controls.ToolTip.SetPlacement(SliderTrack.EndSliderThumb, PlacementMode.Right);
}
}
}
@@ -653,7 +653,7 @@ public class Slider : RangeBase,
{
if (SliderTrack is not null && SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(Value));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(Value));
}
}
else if (change.Property == RangeValueProperty)
@@ -662,12 +662,12 @@ public class Slider : RangeBase,
{
if (SliderTrack.StartSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(RangeValue.StartValue));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.StartSliderThumb, FormatValue(RangeValue.StartValue));
}
if (SliderTrack.EndSliderThumb is not null)
{
ToolTip.SetTip(SliderTrack.EndSliderThumb, FormatValue(RangeValue.EndValue));
Avalonia.Controls.ToolTip.SetTip(SliderTrack.EndSliderThumb, FormatValue(RangeValue.EndValue));
}
}
}
+4 -4
View File
@@ -31,8 +31,8 @@ internal class SliderTheme : BaseControlTheme
};
CreateTemplateParentBinding(startSliderThumb, SliderThumb.IsMotionEnabledProperty,
Slider.IsMotionEnabledProperty);
ToolTip.SetPlacement(startSliderThumb, PlacementMode.Top);
ToolTip.SetShowDelay(startSliderThumb, 0);
Avalonia.Controls.ToolTip.SetPlacement(startSliderThumb, PlacementMode.Top);
Avalonia.Controls.ToolTip.SetShowDelay(startSliderThumb, 0);
startSliderThumb.RegisterInNameScope(scope);
var endSliderThumb = new SliderThumb
@@ -43,8 +43,8 @@ internal class SliderTheme : BaseControlTheme
Slider.IsMotionEnabledProperty);
endSliderThumb.RegisterInNameScope(scope);
ToolTip.SetPlacement(endSliderThumb, PlacementMode.Top);
ToolTip.SetShowDelay(endSliderThumb, 0);
Avalonia.Controls.ToolTip.SetPlacement(endSliderThumb, PlacementMode.Top);
Avalonia.Controls.ToolTip.SetShowDelay(endSliderThumb, 0);
var sliderTrack = new SliderTrack
{
File diff suppressed because it is too large Load Diff
+17 -18
View File
@@ -1,8 +1,9 @@
using System.Reactive.Disposables;
using AtomUI.Controls.Utils;
using AtomUI.MotionScene;
using AtomUI.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Threading;
@@ -28,8 +29,8 @@ internal sealed class ToolTipService : IDisposable
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_subscriptions = new CompositeDisposable(
inputManager.Process.Subscribe(HandleInputManagerOnProcess),
ToolTip.ServiceEnabledProperty.Changed.Subscribe(ServiceEnabledChanged),
ToolTip.TipProperty.Changed.Subscribe(TipChanged));
ToolTip.ServiceEnabledProperty.Changed.Subscribe(HandleServiceEnabledChanged),
ToolTip.TipProperty.Changed.Subscribe(HandleTipChanged));
}
public void Dispose()
@@ -59,9 +60,7 @@ internal sealed class ToolTipService : IDisposable
{
case RawPointerEventType.Move:
var inputHitTestResult =
pointerEventArgs
.GetPropertyOrThrow<(IInputElement? element, IInputElement? firstEnabledAncestor)>(
"InputHitTestResult");
pointerEventArgs.GetInputHitTestResult();
Update(pointerEventArgs.Root, inputHitTestResult.element as Visual);
break;
case RawPointerEventType.LeaveWindow
@@ -96,12 +95,12 @@ internal sealed class ToolTipService : IDisposable
public void Update(IInputRoot root, Visual? candidateToolTipHost)
{
var currentToolTip = _tipControl?.GetValue(ToolTip.ToolTipProperty);
if (root is SceneLayer || root is PopupShadowLayer)
if (root is SceneLayer || root is PopupRoot)
{
return;
}
if (root == currentToolTip?.GetVisualRoot())
if (root == currentToolTip?.PopupHost?.HostedVisualTreeRoot)
{
// Don't update while the pointer is over a tooltip
return;
@@ -109,9 +108,9 @@ internal sealed class ToolTipService : IDisposable
while (candidateToolTipHost != null)
{
if (candidateToolTipHost ==
currentToolTip) // when OverlayPopupHost is in use, the tooltip is in the same window as the host control
if (candidateToolTipHost == currentToolTip)
{
// when OverlayPopupHost is in use, the tooltip is in the same window as the host control
return;
}
@@ -143,7 +142,7 @@ internal sealed class ToolTipService : IDisposable
_tipControl = newControl;
}
private void ServiceEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> args)
private void HandleServiceEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> args)
{
if (args.Sender == _tipControl && !ToolTip.GetServiceEnabled(_tipControl))
{
@@ -155,7 +154,7 @@ internal sealed class ToolTipService : IDisposable
/// called when the <see cref="ToolTip.TipProperty" /> property changes on a control.
/// </summary>
/// <param name="e">The event args.</param>
private void TipChanged(AvaloniaPropertyChangedEventArgs e)
private void HandleTipChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
@@ -215,17 +214,17 @@ internal sealed class ToolTipService : IDisposable
}
}
private void ToolTipClosed(object? sender, EventArgs e)
private void HandleToolTipClosed(object? sender, EventArgs e)
{
_lastTipCloseTime = DateTime.UtcNow.Ticks;
if (sender is ToolTip toolTip)
{
toolTip.Closed -= ToolTipClosed;
toolTip.PointerExited -= ToolTipPointerExited;
toolTip.Closed -= HandleToolTipClosed;
toolTip.PointerExited -= HandleToolTipPointerExited;
}
}
private void ToolTipPointerExited(object? sender, PointerEventArgs e)
private void HandleToolTipPointerExited(object? sender, PointerEventArgs e)
{
// The pointer has exited the tooltip. Close the tooltip unless the current tooltip source is still the
// adorned control.
@@ -257,8 +256,8 @@ internal sealed class ToolTipService : IDisposable
if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip)
{
tooltip.Closed += ToolTipClosed;
tooltip.PointerExited += ToolTipPointerExited;
tooltip.Closed += HandleToolTipClosed;
tooltip.PointerExited += HandleToolTipPointerExited;
}
}
}
+24 -22
View File
@@ -1,11 +1,10 @@
using AtomUI.Data;
using AtomUI.Theme;
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
@@ -28,21 +27,25 @@ internal class ToolTipTheme : BaseControlTheme
{
Name = ToolTipContainerPart
};
if (tip.Content is string text)
{
arrowDecoratedBox.Content = new SingleLineText()
{
Text = text,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
};
}
else if (tip.Content is Control control)
{
arrowDecoratedBox.Content = control;
}
BindUtils.RelayBind(tip, ToolTip.IsShowArrowEffectiveProperty, arrowDecoratedBox, ArrowDecoratedBox.IsShowArrowProperty);
CreateTemplateParentBinding(arrowDecoratedBox, ArrowDecoratedBox.ContentProperty, ToolTip.ContentProperty, BindingPriority.Template,
BindingMode.Default, new FuncValueConverter<object?, object?>(o =>
{
if (o is string text)
{
return new SingleLineText
{
Text = text,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
LineHeight = double.NaN, // 防止有继承属性干扰
};
}
return o;
}));
CreateTemplateParentBinding(arrowDecoratedBox, ArrowDecoratedBox.ContentTemplateProperty, ToolTip.ContentTemplateProperty);
arrowDecoratedBox.RegisterInNameScope(scope);
return arrowDecoratedBox;
});
@@ -50,10 +53,9 @@ internal class ToolTipTheme : BaseControlTheme
protected override void BuildStyles()
{
this.Add(ToolTip.ShadowsProperty, ToolTipTokenKey.ToolTipShadows);
this.Add(ToolTip.DefaultMarginToAnchorProperty, ToolTipTokenKey.MarginToAnchor);
this.Add(ToolTip.MotionDurationProperty, ToolTipTokenKey.ToolTipMotionDuration);
this.Add(TemplatedControl.BackgroundProperty, SharedTokenKey.ColorTransparent);
// this.Add(ToolTipX.ShadowsProperty, ToolTipTokenKey.ToolTipShadows);
// this.Add(ToolTipX.DefaultMarginToAnchorProperty, ToolTipTokenKey.MarginToAnchor);
// this.Add(ToolTipX.MotionDurationProperty, ToolTipTokenKey.ToolTipMotionDuration);
var arrowDecoratedBoxStyle = new Style(selector => selector.Nesting().Template().OfType<ArrowDecoratedBox>());
arrowDecoratedBoxStyle.Add(TemplatedControl.FontSizeProperty, SharedTokenKey.FontSize);
@@ -49,11 +49,6 @@ internal class ToolTipToken : AbstractControlDesignToken
/// </summary>
public TimeSpan ToolTipMotionDuration { get; set; }
/// <summary>
/// 默认距离锚控件的间距
/// </summary>
public double MarginToAnchor { get; set; }
/// <summary>
/// ToolTip 箭头三角形大小
/// </summary>
@@ -73,6 +68,5 @@ internal class ToolTipToken : AbstractControlDesignToken
ToolTipPadding = new Thickness(SharedToken.PaddingSM, SharedToken.PaddingSM / 2 + 2);
ToolTipShadows = SharedToken.BoxShadowsSecondary;
ToolTipMotionDuration = SharedToken.MotionDurationMid;
MarginToAnchor = SharedToken.MarginXXS / 2;
}
}
@@ -0,0 +1,20 @@
using System.Reflection;
using AtomUI.Reflection;
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace AtomUI.Controls.Utils;
internal static class RawPointerEventTypeReflectionExtensions
{
#region 反射信息定义
private static readonly Lazy<PropertyInfo> InputHitTestResultPropertyInfo = new Lazy<PropertyInfo>(
typeof(RawPointerEventArgs).GetPropertyInfoOrThrow("InputHitTestResult",
BindingFlags.Instance | BindingFlags.NonPublic));
#endregion
public static (IInputElement? element, IInputElement? firstEnabledAncestor) GetInputHitTestResult(this RawPointerEventArgs rawPointerEventArgs)
{
return rawPointerEventArgs.GetPropertyOrThrow<(IInputElement?, IInputElement?)>(InputHitTestResultPropertyInfo.Value);
}
}