From 369501d4000c44c1d801ea45289930093ff3aac1 Mon Sep 17 00:00:00 2001 From: polarboy Date: Mon, 7 Oct 2024 22:43:03 +0800 Subject: [PATCH] Refactor Tooltip Motion --- src/AtomUI.Base/MotionScene/AbstractMotion.cs | 12 +- .../MotionScene/INotifyCaptureGhostBitmap.cs | 1 + src/AtomUI.Base/MotionScene/MotionInvoker.cs | 14 +- src/AtomUI.Base/MotionScene/MoveMotions.cs | 60 +++++++ src/AtomUI.Base/MotionScene/SceneLayer.cs | 13 +- .../MotionScene/SceneMotionActorControl.cs | 42 ++++- src/AtomUI.Base/MotionScene/ZoomMotions.cs | 8 +- src/AtomUI.Controls/Popup/Popup.cs | 162 ++++++++---------- src/AtomUI.Controls/Popup/PopupMotionActor.cs | 74 ++++---- .../Primitives/MotionGhostControl.cs | 8 +- src/AtomUI.Controls/Tooltip/ToolTip.cs | 4 +- 11 files changed, 241 insertions(+), 157 deletions(-) diff --git a/src/AtomUI.Base/MotionScene/AbstractMotion.cs b/src/AtomUI.Base/MotionScene/AbstractMotion.cs index fe3220c..fafce1e 100644 --- a/src/AtomUI.Base/MotionScene/AbstractMotion.cs +++ b/src/AtomUI.Base/MotionScene/AbstractMotion.cs @@ -35,24 +35,24 @@ internal class AbstractMotion : IMotion { using var originRestore = new RenderTransformOriginRestore(actor); actor.RenderTransformOrigin = RenderTransformOrigin; + actor.NotifyMotionPreStart(); + NotifyPreStart(); if (aboutToStart is not null) { aboutToStart(); } - actor.NotifyMotionPreStart(); - NotifyPreStart(); - + actor.IsVisible = true; foreach (var animation in Animations) { await animation.RunAsync(actor, cancellationToken); } - + + actor.NotifyMotionCompleted(); + NotifyCompleted(); if (completedAction is not null) { completedAction(); } - actor.NotifyMotionCompleted(); - NotifyCompleted(); }); } diff --git a/src/AtomUI.Base/MotionScene/INotifyCaptureGhostBitmap.cs b/src/AtomUI.Base/MotionScene/INotifyCaptureGhostBitmap.cs index 3477e1b..d9a0663 100644 --- a/src/AtomUI.Base/MotionScene/INotifyCaptureGhostBitmap.cs +++ b/src/AtomUI.Base/MotionScene/INotifyCaptureGhostBitmap.cs @@ -3,4 +3,5 @@ internal interface INotifyCaptureGhostBitmap { public void NotifyCaptureGhostBitmap(); + public void NotifyClearGhostBitmap(); } \ No newline at end of file diff --git a/src/AtomUI.Base/MotionScene/MotionInvoker.cs b/src/AtomUI.Base/MotionScene/MotionInvoker.cs index 300e0ef..c834e20 100644 --- a/src/AtomUI.Base/MotionScene/MotionInvoker.cs +++ b/src/AtomUI.Base/MotionScene/MotionInvoker.cs @@ -44,32 +44,32 @@ internal static class MotionInvoker Action? completedAction = null, CancellationToken cancellationToken = default) { + actor.BuildGhost(); SceneLayer sceneLayer = PrepareSceneLayer(motion, actor); var compositeDisposable = new CompositeDisposable(); compositeDisposable.Add(Disposable.Create(sceneLayer, (state) => { Dispatcher.UIThread.Invoke(async () => { - await Task.Delay(300); + await Task.Delay(100); sceneLayer.Hide(); sceneLayer.Dispose(); }); })); - var ghost = actor.GetAnimatableGhost(); - sceneLayer.SetMotionTarget(ghost); - actor.NotifyMotionTargetAddedToScene(ghost); + sceneLayer.SetMotionActor(actor); + actor.NotifyMotionTargetAddedToScene(); sceneLayer.Show(); sceneLayer.Topmost = true; actor.NotifySceneShowed(); - + actor.IsVisible = false; + await motion.RunAsync(actor, aboutToStart, () => { + compositeDisposable.Dispose(); if (completedAction is not null) { completedAction(); } - - compositeDisposable.Dispose(); }, cancellationToken); } diff --git a/src/AtomUI.Base/MotionScene/MoveMotions.cs b/src/AtomUI.Base/MotionScene/MoveMotions.cs index af6b348..1a990ec 100644 --- a/src/AtomUI.Base/MotionScene/MoveMotions.cs +++ b/src/AtomUI.Base/MotionScene/MoveMotions.cs @@ -88,6 +88,16 @@ internal class MoveDownInMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Height * 2); + } + + internal override Point CalculateScenePosition(Size actorSize, Point actorPosition) + { + return actorPosition.WithY(actorPosition.Y + actorSize.Height); + } } internal class MoveDownOutMotion : AbstractMotion @@ -173,6 +183,11 @@ internal class MoveDownOutMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Height * 2); + } } internal class MoveUpInMotion : AbstractMotion @@ -257,6 +272,16 @@ internal class MoveUpInMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Height * 2); + } + + internal override Point CalculateScenePosition(Size actorSize, Point actorPosition) + { + return actorPosition.WithY(actorPosition.Y - actorSize.Height); + } } internal class MoveUpOutMotion : AbstractMotion @@ -351,6 +376,11 @@ internal class MoveUpOutMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Height * 2); + } } internal class MoveLeftInMotion : AbstractMotion @@ -438,6 +468,16 @@ internal class MoveLeftInMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithWidth(actorSize.Width * 2); + } + + internal override Point CalculateScenePosition(Size actorSize, Point actorPosition) + { + return actorPosition.WithX(actorPosition.X - actorSize.Width); + } } internal class MoveLeftOutMotion : AbstractMotion @@ -521,6 +561,11 @@ internal class MoveLeftOutMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Width * 2); + } } internal class MoveRightInMotion : AbstractMotion @@ -606,6 +651,16 @@ internal class MoveRightInMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Width * 2); + } + + internal override Point CalculateScenePosition(Size actorSize, Point actorPosition) + { + return actorPosition.WithY(actorPosition.X + actorSize.Height); + } } internal class MoveRightOutMotion : AbstractMotion @@ -690,4 +745,9 @@ internal class MoveRightOutMotion : AbstractMotion Animations.Add(animation); } + + internal override Size CalculateSceneSize(Size actorSize) + { + return actorSize.WithHeight(actorSize.Width * 2); + } } \ No newline at end of file diff --git a/src/AtomUI.Base/MotionScene/SceneLayer.cs b/src/AtomUI.Base/MotionScene/SceneLayer.cs index c1b9d99..9db6e99 100644 --- a/src/AtomUI.Base/MotionScene/SceneLayer.cs +++ b/src/AtomUI.Base/MotionScene/SceneLayer.cs @@ -14,6 +14,7 @@ internal class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable private readonly IManagedPopupPositionerPopup? _managedPopupPositionerPopup; private static readonly FieldInfo ManagedPopupPositionerPopupInfo; private readonly Canvas _layout; + private SceneMotionActorControl? _motionActorControl; static SceneLayer() { @@ -88,9 +89,10 @@ internal class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable PlatformImpl?.Dispose(); } - public void SetMotionTarget(Control motionTarget) + public void SetMotionActor(SceneMotionActorControl actorControl) { - _layout.Children.Add(motionTarget); + _motionActorControl = actorControl; + _layout.Children.Add(actorControl); } // 这个地方我们可以需要定制 @@ -149,12 +151,9 @@ internal class SceneLayer : WindowBase, IHostedVisualTreeRoot, IDisposable protected override void OnOpened(EventArgs e) { base.OnOpened(e); - foreach (var child in _layout.Children) + if (_motionActorControl is not null) { - if (child is INotifyCaptureGhostBitmap captureGhost) - { - captureGhost.NotifyCaptureGhostBitmap(); - } + _motionActorControl.NotifySceneLayerHostWinOpened(); } } } \ No newline at end of file diff --git a/src/AtomUI.Base/MotionScene/SceneMotionActorControl.cs b/src/AtomUI.Base/MotionScene/SceneMotionActorControl.cs index 19bef3f..471cca1 100644 --- a/src/AtomUI.Base/MotionScene/SceneMotionActorControl.cs +++ b/src/AtomUI.Base/MotionScene/SceneMotionActorControl.cs @@ -7,6 +7,15 @@ namespace AtomUI.MotionScene; internal class SceneMotionActorControl : MotionActorControl { public event EventHandler? SceneShowed; + + #region 公共属性定义 + + /// + /// 动画实体 + /// + public Control MotionTarget { get; set; } + + #endregion #region 内部属性定义 @@ -18,8 +27,14 @@ internal class SceneMotionActorControl : MotionActorControl #endregion protected Control? _ghost; + + public SceneMotionActorControl(Control motionTarget) + { + MotionTarget = motionTarget; + UseRenderTransform = true; + } - protected virtual void BuildGhost() + internal virtual void BuildGhost() { } @@ -36,6 +51,7 @@ internal class SceneMotionActorControl : MotionActorControl /// /// 在这个接口中,Actor 根据自己的需求对 sceneLayer 进行设置,主要就是位置和大小 /// + /// /// public virtual void NotifySceneLayerCreated(AbstractMotion motion, SceneLayer sceneLayer) { @@ -61,15 +77,31 @@ internal class SceneMotionActorControl : MotionActorControl /// /// 当动画目标控件被添加到动画场景中之后调用,这里需要根据 Motion 的种类设置初始位置和大小 /// - /// - public virtual void NotifyMotionTargetAddedToScene(Control motionTarget) + public virtual void NotifyMotionTargetAddedToScene() { - Canvas.SetLeft(motionTarget, 0); - Canvas.SetTop(motionTarget, 0); + Canvas.SetLeft(this, 0); + Canvas.SetTop(this, 0); } public virtual void NotifySceneShowed() { SceneShowed?.Invoke(this, EventArgs.Empty); } + + internal virtual void NotifySceneLayerHostWinOpened() + { + if (_ghost is INotifyCaptureGhostBitmap captureGhost) + { + captureGhost.NotifyCaptureGhostBitmap(); + } + } + + internal override void NotifyMotionCompleted() + { + base.NotifyMotionCompleted(); + if (_ghost is INotifyCaptureGhostBitmap notifyCaptureGhostBitmap) + { + notifyCaptureGhostBitmap.NotifyClearGhostBitmap(); + } + } } \ No newline at end of file diff --git a/src/AtomUI.Base/MotionScene/ZoomMotions.cs b/src/AtomUI.Base/MotionScene/ZoomMotions.cs index 00efa48..5bdf48d 100644 --- a/src/AtomUI.Base/MotionScene/ZoomMotions.cs +++ b/src/AtomUI.Base/MotionScene/ZoomMotions.cs @@ -151,7 +151,7 @@ internal class ZoomBigInMotion : AbstractMotion var transformSetter = new Setter { Property = MotionActorControl.MotionTransformProperty, - Value = BuildScaleTransform(0.8) + Value = BuildScaleTransform(0.85) }; startFrame.Setters.Add(transformSetter); } @@ -177,7 +177,7 @@ internal class ZoomBigInMotion : AbstractMotion endFrame.Setters.Add(transformSetter); } animation.Children.Add(endFrame); - RenderTransformOrigin = new RelativePoint(0.0, 0.0, RelativeUnit.Relative); + RenderTransformOrigin = new RelativePoint(1.0, 1.0, RelativeUnit.Relative); Animations.Add(animation); } } @@ -231,12 +231,12 @@ internal class ZoomBigOutMotion : AbstractMotion var transformSetter = new Setter { Property = MotionActorControl.MotionTransformProperty, - Value = BuildScaleTransform(0.8) + Value = BuildScaleTransform(0.85) }; endFrame.Setters.Add(transformSetter); } animation.Children.Add(endFrame); - RenderTransformOrigin = new RelativePoint(0.0, 0.0, RelativeUnit.Relative); + RenderTransformOrigin = new RelativePoint(1.0, 1.0, RelativeUnit.Relative); Animations.Add(animation); } } diff --git a/src/AtomUI.Controls/Popup/Popup.cs b/src/AtomUI.Controls/Popup/Popup.cs index 072bf45..6997184 100644 --- a/src/AtomUI.Controls/Popup/Popup.cs +++ b/src/AtomUI.Controls/Popup/Popup.cs @@ -1,10 +1,12 @@ using System.Reactive.Disposables; using AtomUI.Data; +using AtomUI.MotionScene; using AtomUI.Theme.Styling; using AtomUI.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; @@ -78,7 +80,7 @@ public class Popup : AvaloniaPopup AffectsMeasure(PlacementProperty); AffectsMeasure(PlacementAnchorProperty); AffectsMeasure(PlacementGravityProperty); - + IsLightDismissEnabledProperty.OverrideDefaultValue(false); } @@ -159,7 +161,7 @@ public class Popup : AvaloniaPopup AdjustPopupHostPosition(placementTarget!); } } - + if (!_animating) { CreateShadowLayer(); @@ -173,15 +175,16 @@ public class Popup : AvaloniaPopup } } } - + private bool _firstDetected = true; - + private void HandleMouseClick(RawInputEventArgs args) { if (!IsOpen) { return; } + if (args is RawPointerEventArgs pointerEventArgs) { if (pointerEventArgs.Type == RawPointerEventType.LeftButtonUp) @@ -191,6 +194,7 @@ public class Popup : AvaloniaPopup _firstDetected = false; return; } + if (this is IPopupHostProvider popupHostProvider) { if (popupHostProvider.PopupHost != pointerEventArgs.Root) @@ -320,10 +324,10 @@ public class Popup : AvaloniaPopup new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))) .ToArray(); } - + return Array.Empty(); } - + // TODO review 后可能需要删除 private static Rect GetParentClientAreaScreenGeometry(TopLevel topLevel) { @@ -461,49 +465,41 @@ public class Popup : AvaloniaPopup { return; } - Open(); - return; - // _animating = true; - // - // var placementTarget = GetEffectivePlacementTarget(); - // - // Open(); - // - // 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 topLevel = TopLevel.GetTopLevel(placementTarget); - // var scaling = topLevel?.RenderScaling ?? 1.0; - // - // // 调度动画 - // var director = Director.Instance; - // var motion = new ZoomBigInMotion(); - // motion.ConfigureOpacity(MotionDuration); - // motion.ConfigureRenderTransform(MotionDuration); - // - // var motionActor = - // new PopupMotionActor(MaskShadows, offset, scaling, Child ?? popupRoot, motion); - // motionActor.DispatchInSceneLayer = true; - // motionActor.SceneParent = topLevel; - // - // motionActor.Completed += (sender, args) => - // { - // CreateShadowLayer(); - // popupRoot.Show(); - // - // if (RequestCloseWhereAnimationCompleted) - // { - // RequestCloseWhereAnimationCompleted = false; - // Dispatcher.UIThread.Post(() => { CloseAnimation(); }); - // } - // - // _animating = false; - // }; - // director?.Schedule(motionActor); + _animating = true; + + var placementTarget = GetEffectivePlacementTarget(); + + Open(); + + 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 topLevel = TopLevel.GetTopLevel(placementTarget); + var scaling = topLevel?.RenderScaling ?? 1.0; + + // 调度动画 + var motion = new ZoomBigInMotion(MotionDuration); + + var motionActor = new PopupMotionActor(MaskShadows, offset, scaling, Child ?? popupRoot); + motionActor.SceneParent = topLevel; + + MotionInvoker.InvokeInPopupLayer(motionActor, motion, null, () => + { + CreateShadowLayer(); + popupRoot.Show(); + + if (RequestCloseWhereAnimationCompleted) + { + RequestCloseWhereAnimationCompleted = false; + Dispatcher.UIThread.Post(() => { CloseAnimation(); }); + } + + _animating = false; + }); } public void CloseAnimation(Action? closed = null) @@ -513,51 +509,41 @@ public class Popup : AvaloniaPopup RequestCloseWhereAnimationCompleted = true; return; } - + if (!IsOpen) { return; } - Close(); - - // _animating = true; - // - // var director = Director.Instance; - // var motion = new ZoomBigOutMotion(); - // motion.ConfigureOpacity(MotionDuration); - // motion.ConfigureRenderTransform(MotionDuration); - // - // var popupRoot = (Host as PopupRoot)!; - // 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; - // - // var motionActor = new PopupMotionActor(MaskShadows, offset, scaling, Child ?? popupRoot, motion); - // motionActor.DispatchInSceneLayer = true; - // motionActor.SceneParent = topLevel; - // - // motionActor.SceneShowed += (sender, args) => - // { - // HideShadowLayer(); - // popupRoot.Opacity = 0; - // }; - // - // motionActor.Completed += (sender, args) => - // { - // _animating = false; - // _isNeedFlip = true; - // Close(); - // if (closed is not null) - // { - // closed(); - // } - // }; - // - // director?.Schedule(motionActor); + _animating = true; + + var motion = new ZoomBigOutMotion(MotionDuration); + + var popupRoot = (Host as PopupRoot)!; + 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; + + var motionActor = new PopupMotionActor(MaskShadows, offset, scaling, Child ?? popupRoot); + motionActor.SceneParent = topLevel; + + MotionInvoker.InvokeInPopupLayer(motionActor, motion, () => + { + HideShadowLayer(); + popupRoot.Opacity = 0; + }, () => + { + _animating = false; + _isNeedFlip = true; + Close(); + if (closed is not null) + { + closed(); + } + }); } } diff --git a/src/AtomUI.Controls/Popup/PopupMotionActor.cs b/src/AtomUI.Controls/Popup/PopupMotionActor.cs index c26d6ee..eba62af 100644 --- a/src/AtomUI.Controls/Popup/PopupMotionActor.cs +++ b/src/AtomUI.Controls/Popup/PopupMotionActor.cs @@ -7,40 +7,40 @@ using Avalonia.Media; namespace AtomUI.Controls; -// internal class PopupMotionActor : MotionActor -// { -// private readonly BoxShadows _boxShadows; -// private readonly Point _offset; -// private readonly double _scaling; -// -// public PopupMotionActor(BoxShadows boxShadows, -// Point offset, -// double scaling, -// Control motionTarget, -// AbstractMotion motion) -// : base(motionTarget, motion) -// { -// _offset = offset; -// _scaling = scaling; -// _boxShadows = boxShadows; -// } -// -// protected override Point CalculateTopLevelGhostPosition() -// { -// var boxShadowsThickness = _boxShadows.Thickness(); -// var winPos = _offset; // TODO 可能需要乘以 scaling -// var scaledThickness = boxShadowsThickness * _scaling; -// return new Point(winPos.X - scaledThickness.Left, winPos.Y - scaledThickness.Top); -// } -// -// protected override void BuildGhost() -// { -// if (_ghost is null) -// { -// _ghost = new MotionGhostControl(MotionTarget, _boxShadows) -// { -// Shadows = _boxShadows -// }; -// } -// } -// } \ No newline at end of file +internal class PopupMotionActor : SceneMotionActorControl +{ + private readonly BoxShadows _boxShadows; + private readonly Point _offset; + private readonly double _scaling; + + public PopupMotionActor(BoxShadows boxShadows, + Point offset, + double scaling, + Control motionTarget) + : base(motionTarget) + { + _offset = offset; + _scaling = scaling; + _boxShadows = boxShadows; + } + + protected override Point CalculateTopLevelGhostPosition() + { + var boxShadowsThickness = _boxShadows.Thickness(); + var winPos = _offset; // TODO 可能需要乘以 scaling + var scaledThickness = boxShadowsThickness * _scaling; + return new Point(winPos.X - scaledThickness.Left, winPos.Y - scaledThickness.Top); + } + + internal override void BuildGhost() + { + if (_ghost is null) + { + _ghost = new MotionGhostControl(MotionTarget, _boxShadows) + { + Shadows = _boxShadows + }; + Child = _ghost; + } + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/Primitives/MotionGhostControl.cs b/src/AtomUI.Controls/Primitives/MotionGhostControl.cs index 5e4f7ad..54968ad 100644 --- a/src/AtomUI.Controls/Primitives/MotionGhostControl.cs +++ b/src/AtomUI.Controls/Primitives/MotionGhostControl.cs @@ -149,7 +149,7 @@ internal class MotionGhostControl : Control, INotifyCaptureGhostBitmap offsetX += _maskOffset.X; offsetY += _maskOffset.Y; - + // 不知道这里为啥不行 var renderers = new List(); for (var i = 0; i < shadows.Count; ++i) { @@ -217,4 +217,10 @@ internal class MotionGhostControl : Control, INotifyCaptureGhostBitmap _layout!.Children.Clear(); } } + + public void NotifyClearGhostBitmap() + { + _ghostBitmap = null; + InvalidateVisual(); + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Tooltip/ToolTip.cs b/src/AtomUI.Controls/Tooltip/ToolTip.cs index 0a0e24f..4300e17 100644 --- a/src/AtomUI.Controls/Tooltip/ToolTip.cs +++ b/src/AtomUI.Controls/Tooltip/ToolTip.cs @@ -10,7 +10,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Threading; namespace AtomUI.Controls; @@ -678,7 +677,7 @@ public class ToolTip : TemplatedControl, IShadowMaskInfoProvider { popupHostProvider.PopupHostChanged += HandlePopupHostChanged; } - + // TODO 可能是多余的,因为有那个对反转事件的处理 SetupArrowPosition(placement); // 后期看能不能检测对应字段的改变 @@ -728,6 +727,7 @@ public class ToolTip : TemplatedControl, IShadowMaskInfoProvider { return; } + if (_popup is IPopupHostProvider popupHostProvider) { popupHostProvider.PopupHostChanged -= HandlePopupHostChanged;