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#[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 pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_OPEN]);
65}
66
67ferroui_base::ferro_properties! { impl ToolTip {
68 ferro_property!(
69 pub fn tip_property() -> AttachedProperty<Option<BoxedValue>> {
71 FerroProperty::register_attached::<ToolTip, Control, _>("Tip", None)
72 }
73 );
74
75 ferro_property!(
76 pub fn is_open_property() -> AttachedProperty<bool> {
78 FerroProperty::register_attached::<ToolTip, Control, _>("IsOpen", false)
79 }
80 );
81
82 ferro_property!(
83 pub fn placement_property() -> AttachedProperty<PlacementMode> {
85 FerroProperty::register_attached::<ToolTip, Control, _>("Placement", PlacementMode::Pointer)
86 }
87 );
88
89 ferro_property!(
90 pub fn horizontal_offset_property() -> AttachedProperty<f64> {
92 FerroProperty::register_attached::<ToolTip, Control, _>("HorizontalOffset", 0.0)
93 }
94 );
95
96 ferro_property!(
97 pub fn vertical_offset_property() -> AttachedProperty<f64> {
99 FerroProperty::register_attached::<ToolTip, Control, _>("VerticalOffset", 20.0)
100 }
101 );
102
103 ferro_property!(
104 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 pub fn show_delay_property() -> AttachedProperty<i32> {
114 FerroProperty::register_attached::<ToolTip, Control, _>("ShowDelay", 400)
115 }
116 );
117
118 ferro_property!(
119 pub fn between_show_delay_property() -> AttachedProperty<i32> {
121 FerroProperty::register_attached::<ToolTip, Control, _>("BetweenShowDelay", 100)
122 }
123 );
124
125 ferro_property!(
126 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 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 pub fn should_use_overlay_layer_property() -> AttachedProperty<bool> {
149 FerroProperty::register_attached::<ToolTip, Control, _>("ShouldUseOverlayLayer", false)
150 }
151 );
152
153 ferro_property!(
154 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 pub fn tool_tip_opening_event() -> RoutedEvent<CancelRoutedEventArgs> {
179 RoutedEvent::register::<ToolTip, CancelRoutedEventArgs>("ToolTipOpening", RoutingStrategies::DIRECT)
180 }
181 );
182
183 ferro_routed_event!(
184 pub fn tool_tip_closing_event() -> RoutedEvent<RoutedEventArgs> {
191 RoutedEvent::register::<ToolTip, RoutedEventArgs>("ToolTipClosing", RoutingStrategies::DIRECT)
192 }
193 );
194
195 fn static_constructor() {
197 let _ = Self::is_open_property().changed().subscribe(Self::is_open_changed);
199 }
200
201 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 pub fn get_tip(element: &Control) -> Option<BoxedValue> {
220 element.get_value(Self::tip_property())
221 }
222
223 pub fn set_tip(element: &Control, value: Option<BoxedValue>) {
226 element.set_value(Self::tip_property(), value)
227 }
228
229 pub fn get_is_open(element: &Control) -> bool {
232 element.get_value(Self::is_open_property())
233 }
234
235 pub fn set_is_open(element: &Control, value: bool) {
238 element.set_value(Self::is_open_property(), value)
239 }
240
241 pub fn get_placement(element: &Control) -> PlacementMode {
244 element.get_value(Self::placement_property())
245 }
246
247 pub fn set_placement(element: &Control, value: PlacementMode) {
250 element.set_value(Self::placement_property(), value)
251 }
252
253 pub fn get_horizontal_offset(element: &Control) -> f64 {
255 element.get_value(Self::horizontal_offset_property())
256 }
257
258 pub fn set_horizontal_offset(element: &Control, value: f64) {
260 element.set_value(Self::horizontal_offset_property(), value)
261 }
262
263 pub fn get_vertical_offset(element: &Control) -> f64 {
265 element.get_value(Self::vertical_offset_property())
266 }
267
268 pub fn set_vertical_offset(element: &Control, value: f64) {
270 element.set_value(Self::vertical_offset_property(), value)
271 }
272
273 pub fn get_show_delay(element: &Control) -> i32 {
276 element.get_value(Self::show_delay_property())
277 }
278
279 pub fn set_show_delay(element: &Control, value: i32) {
282 element.set_value(Self::show_delay_property(), value)
283 }
284
285 pub fn get_between_show_delay(element: &Control) -> i32 {
290 element.get_value(Self::between_show_delay_property())
291 }
292
293 pub fn set_between_show_delay(element: &Control, value: i32) {
300 element.set_value(Self::between_show_delay_property(), value)
301 }
302
303 pub fn get_show_on_disabled(element: &Control) -> bool {
305 element.get_value(Self::show_on_disabled_property())
306 }
307
308 pub fn set_show_on_disabled(element: &Control, value: bool) {
310 element.set_value(Self::show_on_disabled_property(), value)
311 }
312
313 pub fn get_service_enabled(element: &Control) -> bool {
316 element.get_value(Self::service_enabled_property())
317 }
318
319 pub fn set_service_enabled(element: &Control, value: bool) {
322 element.set_value(Self::service_enabled_property(), value)
323 }
324
325 pub fn get_should_use_overlay_layer(element: &Control) -> bool {
328 element.get_value(Self::should_use_overlay_layer_property())
329 }
330
331 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 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 pub fn remove_tool_tip_opening_handler(element: &Control, handler: RoutedEventHandlerToken) {
348 element.remove_handler(Self::tool_tip_opening_event(), handler);
349 }
350
351 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 pub fn remove_tool_tip_closing_handler(element: &Control, handler: RoutedEventHandlerToken) {
362 element.remove_handler(Self::tool_tip_closing_event(), handler);
363 }
364
365 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 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 pub fn adorned_control(&self) -> Option<Ref<Control>> {
429 self.adorned_control.borrow().as_ref().and_then(WeakRef::upgrade)
430 }
431
432 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 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 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 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 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 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 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
598fn 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
608pub(crate) fn same_value(a: &BoxedValue, b: &BoxedValue) -> bool {
611 Rc::ptr_eq(a, b) || **a == **b
612}
613
614pub(crate) fn is_tool_tip(value: &BoxedValue) -> bool {
616 tool_tip_from_boxed(value).is_some()
617}
618
619struct 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}