Skip to main content

ferroui_controls/
tool_tip.rs

1use crate::diagnostics::IPopupHostProvider;
2use crate::metadata::PseudoClassesAttribute;
3use crate::primitives::{CustomPopupPlacementCallbackValue, IPopupHost, Popup, TemplatedControlImpl};
4use crate::primitives::popup_positioning::CustomPopupPlacementCallback;
5use crate::{ContentControl, ContentControlImpl, Control, ControlImpl, PlacementMode};
6use ferroui_base::data::BindingPriority;
7use ferroui_base::input::InputElementImpl;
8use ferroui_base::interactivity::{
9    CancelRoutedEventArgs, Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken,
10    RoutingStrategies,
11};
12use ferroui_base::layout::LayoutableImpl;
13use ferroui_base::reactive::{CompositeDisposable, Disposable, IDisposable};
14use ferroui_base::utilities::HandlerList;
15use ferroui_base::{
16    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, AnyValue, AttachedProperty, BoxedValue,
17    FerroObjectExtensions, FerroObjectImpl, FerroProperty, FerroPropertyChangedEventArgs, Ref,
18    StyledElementImpl, StyledPropertyOptions, VisualImpl, WeakRef,
19};
20use std::cell::RefCell;
21use std::rc::Rc;
22
23const PC_OPEN: &str = ":open";
24
25/// A control which pops up a hint when a control is hovered.
26///
27/// You will probably not want to create a [`ToolTip`] control directly: if
28/// added to the tree it will act as a simple [`ContentControl`] styled to
29/// look like a tooltip. To add a tooltip to a control, use the `Tip`
30/// attached property ([`ToolTip::tip_property`]), assigning the content
31/// that you want displayed.
32#[repr(C)]
33pub struct ToolTip {
34    base: ContentControl,
35    popup: RefCell<Option<Ref<Popup>>>,
36    popup_host_changed_handler: HandlerList<dyn Fn(Option<&Rc<dyn IPopupHost>>)>,
37    subscriptions: RefCell<Option<Rc<CompositeDisposable>>>,
38    adorned_control: RefCell<Option<WeakRef<Control>>>,
39    closed: HandlerList<dyn Fn(&ToolTip)>,
40}
41
42ferro_class!(ToolTip: ContentControl);
43ferroui_base::ferro_class_info!(ToolTip { new: ToolTip::new });
44ferro_impl_classes!(
45    ToolTip: StyledElementImpl,
46    VisualImpl,
47    LayoutableImpl,
48    InteractiveImpl,
49    InputElementImpl,
50    TemplatedControlImpl,
51    ContentControlImpl
52);
53
54impl ControlImpl for ToolTip {
55    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
56        crate::automation::peers::ToolTipAutomationPeer::new(this).upcast()
57    }
58}
59
60ferroui_base::ferro_impl_classes!(ToolTip: FerroObjectImpl);
61
62impl ToolTip {
63    /// The pseudoclasses set by the class.
64    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_OPEN]);
65}
66
67ferroui_base::ferro_properties! { impl ToolTip {
68    ferro_property!(
69        /// Defines the `ToolTip.Tip` attached property.
70        pub fn tip_property() -> AttachedProperty<Option<BoxedValue>> {
71            FerroProperty::register_attached::<ToolTip, Control, _>("Tip", None)
72        }
73    );
74
75    ferro_property!(
76        /// Defines the `ToolTip.IsOpen` attached property.
77        pub fn is_open_property() -> AttachedProperty<bool> {
78            FerroProperty::register_attached::<ToolTip, Control, _>("IsOpen", false)
79        }
80    );
81
82    ferro_property!(
83        /// Defines the `ToolTip.Placement` property.
84        pub fn placement_property() -> AttachedProperty<PlacementMode> {
85            FerroProperty::register_attached::<ToolTip, Control, _>("Placement", PlacementMode::Pointer)
86        }
87    );
88
89    ferro_property!(
90        /// Defines the `ToolTip.HorizontalOffset` property.
91        pub fn horizontal_offset_property() -> AttachedProperty<f64> {
92            FerroProperty::register_attached::<ToolTip, Control, _>("HorizontalOffset", 0.0)
93        }
94    );
95
96    ferro_property!(
97        /// Defines the `ToolTip.VerticalOffset` property.
98        pub fn vertical_offset_property() -> AttachedProperty<f64> {
99            FerroProperty::register_attached::<ToolTip, Control, _>("VerticalOffset", 20.0)
100        }
101    );
102
103    ferro_property!(
104        /// Defines the `ToolTip.CustomPopupPlacementCallback` property; see
105        /// [`Popup::custom_popup_placement_callback_property`].
106        pub fn custom_popup_placement_callback_property() -> AttachedProperty<Option<CustomPopupPlacementCallbackValue>> {
107            FerroProperty::register_attached::<ToolTip, Control, _>("CustomPopupPlacementCallback", None)
108        }
109    );
110
111    ferro_property!(
112        /// Defines the `ToolTip.ShowDelay` property.
113        pub fn show_delay_property() -> AttachedProperty<i32> {
114            FerroProperty::register_attached::<ToolTip, Control, _>("ShowDelay", 400)
115        }
116    );
117
118    ferro_property!(
119        /// Defines the `ToolTip.BetweenShowDelay` property.
120        pub fn between_show_delay_property() -> AttachedProperty<i32> {
121            FerroProperty::register_attached::<ToolTip, Control, _>("BetweenShowDelay", 100)
122        }
123    );
124
125    ferro_property!(
126        /// Defines the `ToolTip.ShowOnDisabled` property.
127        pub fn show_on_disabled_property() -> AttachedProperty<bool> {
128            FerroProperty::register_attached_with::<ToolTip, Control, _>(
129                "ShowOnDisabled",
130                StyledPropertyOptions::new(false).inherits(true),
131            )
132        }
133    );
134
135    ferro_property!(
136        /// Defines the `ToolTip.ServiceEnabled` property.
137        pub fn service_enabled_property() -> AttachedProperty<bool> {
138            FerroProperty::register_attached_with::<ToolTip, Control, _>(
139                "ServiceEnabled",
140                StyledPropertyOptions::new(true).inherits(true),
141            )
142        }
143    );
144
145    ferro_property!(
146        /// Defines the `ToolTip.ShouldUseOverlayLayer` property; see
147        /// [`Popup::should_use_overlay_layer`].
148        pub fn should_use_overlay_layer_property() -> AttachedProperty<bool> {
149            FerroProperty::register_attached::<ToolTip, Control, _>("ShouldUseOverlayLayer", false)
150        }
151    );
152
153    ferro_property!(
154        /// Stores the current [`ToolTip`] instance in the control (internal
155        /// upstream).
156        pub fn tool_tip_property() -> AttachedProperty<Option<Ref<ToolTip>>> {
157            FerroProperty::register_attached::<ToolTip, Control, _>("ToolTip", None)
158        }
159    );
160} }
161
162impl ToolTip {
163    ferro_routed_event!(
164        /// The event raised when a tooltip is going to be shown on an
165        /// element.
166        ///
167        /// To prevent a tooltip from appearing in the UI, your handler for
168        /// `ToolTipOpening` can mark the event data as cancelled. Otherwise,
169        /// the tooltip is displayed, using the value of the `Tip` property
170        /// as the tooltip content. Another possible scenario is that you
171        /// could write a handler that resets the value of the `Tip`
172        /// property for the element that is the event source, just before
173        /// the tooltip is displayed. `ToolTipOpening` will not be raised if
174        /// the value of `Tip` is `None` or otherwise unset. Do not
175        /// deliberately set `Tip` to `None` while a tooltip is open or
176        /// opening; this will not have the effect of closing the tooltip,
177        /// and will instead create an undesirable visual artifact in the UI.
178        pub fn tool_tip_opening_event() -> RoutedEvent<CancelRoutedEventArgs> {
179            RoutedEvent::register::<ToolTip, CancelRoutedEventArgs>("ToolTipOpening", RoutingStrategies::DIRECT)
180        }
181    );
182
183    ferro_routed_event!(
184        /// The event raised when a tooltip on an element that was shown
185        /// should now be hidden.
186        ///
187        /// Marking the `ToolTipClosing` event as handled does not cancel
188        /// closing the tooltip. Once the tooltip is displayed, closing the
189        /// tooltip is done only in response to user interaction with the UI.
190        pub fn tool_tip_closing_event() -> RoutedEvent<RoutedEventArgs> {
191            RoutedEvent::register::<ToolTip, RoutedEventArgs>("ToolTipClosing", RoutingStrategies::DIRECT)
192        }
193    );
194
195    /// Initializes static members of the [`ToolTip`] class.
196    fn static_constructor() {
197        // The subscription lives as long as the property.
198        let _ = Self::is_open_property().changed().subscribe(Self::is_open_changed);
199    }
200
201    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
202    pub fn construct() -> Self {
203        Self {
204            base: ContentControl::construct(),
205            popup: RefCell::new(None),
206            popup_host_changed_handler: HandlerList::new(),
207            subscriptions: RefCell::new(None),
208            adorned_control: RefCell::new(None),
209            closed: HandlerList::new(),
210        }
211    }
212
213    pub fn new() -> Ref<Self> {
214        instantiate(Self::construct())
215    }
216
217    /// Gets the value of the `ToolTip.Tip` attached property: the content
218    /// to be displayed in the tooltip of `element`.
219    pub fn get_tip(element: &Control) -> Option<BoxedValue> {
220        element.get_value(Self::tip_property())
221    }
222
223    /// Sets the value of the `ToolTip.Tip` attached property: the content
224    /// to be displayed in the tooltip of `element`.
225    pub fn set_tip(element: &Control, value: Option<BoxedValue>) {
226        element.set_value(Self::tip_property(), value)
227    }
228
229    /// Gets the value of the `ToolTip.IsOpen` attached property: whether
230    /// the tooltip of `element` is visible.
231    pub fn get_is_open(element: &Control) -> bool {
232        element.get_value(Self::is_open_property())
233    }
234
235    /// Sets the value of the `ToolTip.IsOpen` attached property: whether
236    /// the tooltip of `element` is visible.
237    pub fn set_is_open(element: &Control, value: bool) {
238        element.set_value(Self::is_open_property(), value)
239    }
240
241    /// Gets the value of the `ToolTip.Placement` attached property: how the
242    /// tooltip of `element` is positioned.
243    pub fn get_placement(element: &Control) -> PlacementMode {
244        element.get_value(Self::placement_property())
245    }
246
247    /// Sets the value of the `ToolTip.Placement` attached property: how the
248    /// tooltip of `element` is positioned.
249    pub fn set_placement(element: &Control, value: PlacementMode) {
250        element.set_value(Self::placement_property(), value)
251    }
252
253    /// Gets the value of the `ToolTip.HorizontalOffset` attached property.
254    pub fn get_horizontal_offset(element: &Control) -> f64 {
255        element.get_value(Self::horizontal_offset_property())
256    }
257
258    /// Sets the value of the `ToolTip.HorizontalOffset` attached property.
259    pub fn set_horizontal_offset(element: &Control, value: f64) {
260        element.set_value(Self::horizontal_offset_property(), value)
261    }
262
263    /// Gets the value of the `ToolTip.VerticalOffset` attached property.
264    pub fn get_vertical_offset(element: &Control) -> f64 {
265        element.get_value(Self::vertical_offset_property())
266    }
267
268    /// Sets the value of the `ToolTip.VerticalOffset` attached property.
269    pub fn set_vertical_offset(element: &Control, value: f64) {
270        element.set_value(Self::vertical_offset_property(), value)
271    }
272
273    /// Gets the value of the `ToolTip.ShowDelay` attached property: the
274    /// time, in milliseconds, before the tooltip of `element` opens.
275    pub fn get_show_delay(element: &Control) -> i32 {
276        element.get_value(Self::show_delay_property())
277    }
278
279    /// Sets the value of the `ToolTip.ShowDelay` attached property: the
280    /// time, in milliseconds, before the tooltip of `element` opens.
281    pub fn set_show_delay(element: &Control, value: i32) {
282        element.set_value(Self::show_delay_property(), value)
283    }
284
285    /// Gets the number of milliseconds since the last tooltip closed during
286    /// which the tooltip of `element` will open immediately, or a negative
287    /// value indicating that the tooltip will always wait for the show
288    /// delay before opening.
289    pub fn get_between_show_delay(element: &Control) -> i32 {
290        element.get_value(Self::between_show_delay_property())
291    }
292
293    /// Sets the number of milliseconds since the last tooltip closed during
294    /// which the tooltip of `element` will open immediately.
295    ///
296    /// Setting a negative value disables the immediate opening behaviour.
297    /// The tooltip of `element` will then always wait until the show delay
298    /// elapses before showing.
299    pub fn set_between_show_delay(element: &Control, value: i32) {
300        element.set_value(Self::between_show_delay_property(), value)
301    }
302
303    /// Gets whether a control will display a tooltip even if it disabled.
304    pub fn get_show_on_disabled(element: &Control) -> bool {
305        element.get_value(Self::show_on_disabled_property())
306    }
307
308    /// Sets whether a control will display a tooltip even if it disabled.
309    pub fn set_show_on_disabled(element: &Control, value: bool) {
310        element.set_value(Self::show_on_disabled_property(), value)
311    }
312
313    /// Gets whether showing and hiding of a control's tooltip will be
314    /// automatically controlled by the tooltip service.
315    pub fn get_service_enabled(element: &Control) -> bool {
316        element.get_value(Self::service_enabled_property())
317    }
318
319    /// Sets whether showing and hiding of a control's tooltip will be
320    /// automatically controlled by the tooltip service.
321    pub fn set_service_enabled(element: &Control, value: bool) {
322        element.set_value(Self::service_enabled_property(), value)
323    }
324
325    /// Gets whether the tooltip popup for `element` is shown in the overlay
326    /// layer. See [`Popup::should_use_overlay_layer`] for details.
327    pub fn get_should_use_overlay_layer(element: &Control) -> bool {
328        element.get_value(Self::should_use_overlay_layer_property())
329    }
330
331    /// Sets whether the tooltip popup for `element` is shown in the overlay
332    /// layer. See [`Popup::should_use_overlay_layer`] for details.
333    pub fn set_should_use_overlay_layer(element: &Control, value: bool) {
334        element.set_value(Self::should_use_overlay_layer_property(), value)
335    }
336
337    /// Adds a handler for the `ToolTipOpening` attached event to `element`.
338    pub fn add_tool_tip_opening_handler(
339        element: &Control,
340        handler: impl Fn(&Interactive, &CancelRoutedEventArgs) + 'static,
341    ) -> RoutedEventHandlerToken {
342        element.add_handler(Self::tool_tip_opening_event(), handler)
343    }
344
345    /// Removes a handler for the `ToolTipOpening` attached event from
346    /// `element`.
347    pub fn remove_tool_tip_opening_handler(element: &Control, handler: RoutedEventHandlerToken) {
348        element.remove_handler(Self::tool_tip_opening_event(), handler);
349    }
350
351    /// Adds a handler for the `ToolTipClosing` attached event to `element`.
352    pub fn add_tool_tip_closing_handler(
353        element: &Control,
354        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
355    ) -> RoutedEventHandlerToken {
356        element.add_handler(Self::tool_tip_closing_event(), handler)
357    }
358
359    /// Removes a handler for the `ToolTipClosing` attached event from
360    /// `element`.
361    pub fn remove_tool_tip_closing_handler(element: &Control, handler: RoutedEventHandlerToken) {
362        element.remove_handler(Self::tool_tip_closing_event(), handler);
363    }
364
365    /// Gets the value of the `ToolTip.CustomPopupPlacementCallback`
366    /// attached property.
367    pub fn get_custom_popup_placement_callback(element: &Control) -> Option<CustomPopupPlacementCallback> {
368        element.get_value(Self::custom_popup_placement_callback_property()).map(|value| value.0)
369    }
370
371    /// Sets the value of the `ToolTip.CustomPopupPlacementCallback`
372    /// attached property.
373    pub fn set_custom_popup_placement_callback(element: &Control, value: Option<CustomPopupPlacementCallback>) {
374        element.set_value(
375            Self::custom_popup_placement_callback_property(),
376            value.map(CustomPopupPlacementCallbackValue),
377        )
378    }
379
380    fn is_open_changed(e: &FerroPropertyChangedEventArgs<'_>) {
381        let control = e.sender().downcast_ref::<Control>().expect("the tooltip properties are attached to controls");
382        let new_value = e.get_new_value::<bool>();
383
384        if new_value {
385            let args = CancelRoutedEventArgs::with_event(Self::tool_tip_opening_event());
386            control.raise_event(&args);
387            if args.cancel() {
388                control.set_current_value(Self::is_open_property(), false);
389                return;
390            }
391
392            let Some(tip) = Self::get_tip(control) else {
393                control.set_current_value(Self::is_open_property(), false);
394                return;
395            };
396
397            let tip_tool_tip = tool_tip_from_boxed(&tip);
398            let mut tool_tip = control.get_value(Self::tool_tip_property());
399            let is_current = tool_tip.as_ref().is_some_and(|tool_tip| {
400                tip_tool_tip.as_ref() == Some(tool_tip)
401                    || tool_tip.content().is_some_and(|content| same_value(&content, &tip))
402            });
403            if !is_current {
404                if let Some(tool_tip) = &tool_tip {
405                    tool_tip.close();
406                }
407
408                let new_tool_tip = tip_tool_tip.unwrap_or_else(|| {
409                    let tool_tip = ToolTip::new();
410                    tool_tip.set_content(Some(tip));
411                    tool_tip
412                });
413                control.set_value(Self::tool_tip_property(), Some(new_tool_tip.clone()));
414                tool_tip = Some(new_tool_tip);
415            }
416
417            let tool_tip = tool_tip.expect("the tooltip was just created");
418            let control = control.to_ref();
419            *tool_tip.adorned_control.borrow_mut() = Some(control.downgrade());
420            tool_tip.open(&control);
421        } else if let Some(tool_tip) = control.get_value(Self::tool_tip_property()) {
422            *tool_tip.adorned_control.borrow_mut() = None;
423            tool_tip.close();
424        }
425    }
426
427    /// The control this tooltip is shown for (internal upstream).
428    pub fn adorned_control(&self) -> Option<Ref<Control>> {
429        self.adorned_control.borrow().as_ref().and_then(WeakRef::upgrade)
430    }
431
432    /// Raised when the popup of the tooltip has closed (internal upstream).
433    /// Disposing the returned handle unsubscribes.
434    pub fn closed(&self, handler: impl Fn(&ToolTip) + 'static) -> Rc<dyn IDisposable> {
435        let token = self.closed.add(Rc::new(handler));
436        let weak = self.to_ref().downgrade();
437        Disposable::create(move || {
438            if let Some(this) = weak.upgrade() {
439                this.closed.remove(token);
440            }
441        })
442    }
443
444    /// The host of the popup the tooltip is shown in, while it is open.
445    pub fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
446        let popup = self.popup.borrow().clone();
447        popup.and_then(|popup| popup.host())
448    }
449
450    /// Raised when the popup host changes (the popup host provider
451    /// contract). Disposing the returned handle unsubscribes.
452    pub fn popup_host_changed(
453        &self,
454        handler: impl Fn(Option<&Rc<dyn IPopupHost>>) + 'static,
455    ) -> Rc<dyn IDisposable> {
456        let token = self.popup_host_changed_handler.add(Rc::new(handler));
457        let weak = self.to_ref().downgrade();
458        Disposable::create(move || {
459            if let Some(this) = weak.upgrade() {
460                this.popup_host_changed_handler.remove(token);
461            }
462        })
463    }
464
465    /// The tooltip as a popup host provider (the diagnostics contract it
466    /// implements upstream).
467    pub fn to_popup_host_provider(&self) -> Rc<dyn IPopupHostProvider> {
468        Rc::new(ToolTipPopupHostProvider(self.to_ref()))
469    }
470
471    fn open(&self, control: &Ref<Control>) {
472        self.close();
473
474        let existing = self.popup.borrow().clone();
475        let popup = match existing {
476            Some(popup) => popup,
477            None => {
478                let popup = Popup::new();
479                popup.set_takes_focus_from_native_control(false);
480                popup.set_window_manager_add_shadow_hint(false);
481
482                // The popup belongs to the tooltip, which unsubscribes by
483                // letting go of it.
484                let weak = self.to_ref().downgrade();
485                let _ = popup.opened({
486                    let weak = weak.clone();
487                    let weak_popup = popup.downgrade();
488                    move || {
489                        if let (Some(this), Some(popup)) = (weak.upgrade(), weak_popup.upgrade()) {
490                            this.on_popup_opened(&popup);
491                        }
492                    }
493                });
494                let _ = popup.closed(move || {
495                    if let Some(this) = weak.upgrade() {
496                        this.on_popup_closed();
497                    }
498                });
499
500                *self.popup.borrow_mut() = Some(popup.clone());
501                popup
502            }
503        };
504
505        // The popup holds the tooltip only while it is open: a tooltip and
506        // its popup must not keep each other alive.
507        popup.set_child(self.to_ref());
508
509        let subscriptions = CompositeDisposable::from_disposables([
510            popup.bind_typed_value(
511                Popup::horizontal_offset_property(),
512                control.get_binding_observable(Self::horizontal_offset_property()),
513                BindingPriority::LocalValue,
514            ),
515            popup.bind_typed_value(
516                Popup::vertical_offset_property(),
517                control.get_binding_observable(Self::vertical_offset_property()),
518                BindingPriority::LocalValue,
519            ),
520            popup.bind_typed_value(
521                Popup::placement_property(),
522                control.get_binding_observable(Self::placement_property()),
523                BindingPriority::LocalValue,
524            ),
525            popup.bind_typed_value(
526                Popup::custom_popup_placement_callback_property(),
527                control.get_binding_observable(Self::custom_popup_placement_callback_property()),
528                BindingPriority::LocalValue,
529            ),
530            popup.bind_typed_value(
531                Popup::should_use_overlay_layer_property(),
532                control.get_binding_observable(Self::should_use_overlay_layer_property()),
533                BindingPriority::LocalValue,
534            ),
535        ]);
536        *self.subscriptions.borrow_mut() = Some(Rc::new(subscriptions));
537
538        popup.set_placement_target(control);
539        popup.set_popup_parent(Some(control));
540
541        popup.set_is_open(true);
542    }
543
544    fn close(&self) {
545        if let Some(adorned_control) = self.adorned_control() {
546            if Self::get_is_open(&adorned_control) {
547                let args = RoutedEventArgs::with_event(Self::tool_tip_closing_event());
548                adorned_control.raise_event(&args);
549            }
550        }
551
552        let popup = self.popup.borrow().clone();
553        if let Some(popup) = popup {
554            popup.set_is_open(false);
555            popup.set_popup_parent(None);
556            popup.set_placement_target(None::<Ref<Control>>);
557            if !popup.is_open() {
558                popup.set_child(None::<Ref<Control>>);
559            }
560        }
561
562        let subscriptions = self.subscriptions.borrow().clone();
563        if let Some(subscriptions) = subscriptions {
564            subscriptions.dispose();
565        }
566    }
567
568    fn on_popup_closed(&self) {
569        // This condition is true, when Popup was closed by any other reason outside of ToolTipService/ToolTip, keeping IsOpen=true.
570        if let Some(adorned_control) = self.adorned_control() {
571            if Self::get_is_open(&adorned_control) {
572                adorned_control.set_current_value(Self::is_open_property(), false);
573            }
574        }
575
576        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
577            handler(None);
578        }
579        self.update_pseudo_classes(false);
580        for (_, handler) in self.closed.snapshot().iter() {
581            handler(self);
582        }
583    }
584
585    fn on_popup_opened(&self, popup: &Popup) {
586        let host = popup.host();
587        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
588            handler(host.as_ref());
589        }
590        self.update_pseudo_classes(true);
591    }
592
593    fn update_pseudo_classes(&self, new_value: bool) {
594        self.pseudo_classes().set(PC_OPEN, new_value);
595    }
596}
597
598/// The tooltip held by the untyped value of the `Tip` property, if it holds
599/// one: a tooltip boxed as a control (see [`Control::boxed`]) or as itself.
600fn tool_tip_from_boxed(value: &BoxedValue) -> Option<Ref<ToolTip>> {
601    if let Some(control) = Control::from_boxed(value) {
602        return control.cast::<ToolTip>();
603    }
604    let value: &dyn AnyValue = &**value;
605    value.downcast_ref::<Ref<ToolTip>>().cloned()
606}
607
608/// Whether two untyped values are the same value: the same box, or boxes of
609/// equal values.
610pub(crate) fn same_value(a: &BoxedValue, b: &BoxedValue) -> bool {
611    Rc::ptr_eq(a, b) || **a == **b
612}
613
614/// Whether the untyped value of the `Tip` property holds a tooltip.
615pub(crate) fn is_tool_tip(value: &BoxedValue) -> bool {
616    tool_tip_from_boxed(value).is_some()
617}
618
619/// The popup host provider contract of a tooltip.
620struct ToolTipPopupHostProvider(Ref<ToolTip>);
621
622impl IPopupHostProvider for ToolTipPopupHostProvider {
623    fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
624        self.0.popup_host()
625    }
626
627    fn popup_host_changed(&self, handler: Rc<dyn Fn(Option<&Rc<dyn IPopupHost>>)>) -> Rc<dyn IDisposable> {
628        self.0.popup_host_changed(move |host| handler(host))
629    }
630}