ferroui_controls/
tool_tip_service.rs1use crate::tool_tip::{is_tool_tip, same_value};
2use crate::{Control, IToolTipService, ToolTip};
3use ferroui_base::input::raw::{IRawInputEventArgs, RawPointerEventArgs, RawPointerEventType};
4use ferroui_base::input::{IInputManager, IInputRoot, InputElement, PointerEventArgs};
5use ferroui_base::reactive::{AnonymousObserver, CompositeDisposable, IDisposable};
6use ferroui_base::threading::{Dispatcher, DispatcherPriority, DispatcherTimer};
7use ferroui_base::{BoxedValue, FerroPropertyChangedEventArgs, Ref, Visual, WeakRef};
8use std::cell::{Cell, RefCell};
9use std::rc::{Rc, Weak};
10use std::time::Duration;
11
12pub struct ToolTipService {
14 this: Weak<ToolTipService>,
15 subscriptions: CompositeDisposable,
16
17 tip_control: RefCell<Option<WeakRef<Control>>>,
20 pointer_over_tip: Cell<bool>,
21 pointer_over_owner: Cell<bool>,
22 pending_close_id: Cell<i32>,
23 last_tip_close_time: Cell<Option<i64>>,
26 timer: RefCell<Option<Rc<DispatcherTimer>>>,
27}
28
29impl ToolTipService {
30 pub fn new(input_manager: &Rc<dyn IInputManager>) -> Rc<ToolTipService> {
33 let service = Rc::new_cyclic(|this: &Weak<ToolTipService>| ToolTipService {
34 this: this.clone(),
35 subscriptions: CompositeDisposable::new(),
36 tip_control: RefCell::new(None),
37 pointer_over_tip: Cell::new(false),
38 pointer_over_owner: Cell::new(false),
39 pending_close_id: Cell::new(0),
40 last_tip_close_time: Cell::new(None),
41 timer: RefCell::new(None),
42 });
43
44 let weak = Rc::downgrade(&service);
45 service.subscriptions.add(input_manager.process().subscribe(Rc::new(AnonymousObserver::new({
46 let weak = weak.clone();
47 move |e: Rc<dyn IRawInputEventArgs>| {
48 if let Some(this) = weak.upgrade() {
49 this.input_manager_on_process(&*e);
50 }
51 }
52 }))));
53 service.subscriptions.add(ToolTip::service_enabled_property().changed().subscribe({
54 let weak = weak.clone();
55 move |args| {
56 if let Some(this) = weak.upgrade() {
57 this.service_enabled_changed(args);
58 }
59 }
60 }));
61 service.subscriptions.add(ToolTip::tip_property().changed().subscribe(move |e| {
62 if let Some(this) = weak.upgrade() {
63 this.tip_changed(e);
64 }
65 }));
66
67 service
68 }
69
70 pub fn dispose(&self) {
73 self.stop_timer();
74 self.cancel_pending_close();
75 self.subscriptions.dispose();
76 }
77
78 fn tip_control(&self) -> Option<Ref<Control>> {
79 self.tip_control.borrow().as_ref().and_then(WeakRef::upgrade)
80 }
81
82 fn set_tip_control(&self, value: Option<&Ref<Control>>) {
83 *self.tip_control.borrow_mut() = value.map(Ref::downgrade);
84 }
85
86 fn input_manager_on_process(&self, e: &dyn IRawInputEventArgs) {
87 let Some(pointer_event) = e.downcast_ref::<RawPointerEventArgs>() else { return };
88
89 let tip_control = self.tip_control();
90 let tip_visual_root = tip_control.as_ref().and_then(|control| control.visual_root());
91 let current_tip = tip_control.as_ref().and_then(|control| control.get_value(ToolTip::tool_tip_property()));
92
93 let tip_popup_input_root = current_tip
95 .and_then(|tip| tip.popup_host())
96 .and_then(|host| host.as_control().get_input_root())
97 .filter(|root| root_element_visual(root) != tip_visual_root);
98
99 let is_tip_event = tip_popup_input_root.as_ref().is_some_and(|root| same_input_root(e.root(), root));
100 let is_tip_owner_window_event = root_element_visual(e.root()) == tip_visual_root;
101
102 if is_tip_event || is_tip_owner_window_event {
103 if pointer_event.type_() != RawPointerEventType::LeaveWindow {
106 self.pointer_over_tip.set(is_tip_event);
107 self.pointer_over_owner.set(is_tip_owner_window_event);
108 self.cancel_pending_close();
109 } else if is_tip_event {
110 self.pointer_over_tip.set(false);
111 } else {
112 self.pointer_over_owner.set(false);
113 }
114 }
115
116 match pointer_event.type_() {
117 RawPointerEventType::Move => {
118 let candidate = pointer_event.input_hit_test_result().0.map(|element| element.upcast::<Visual>());
119 IToolTipService::update(self, pointer_event.root(), candidate);
120 }
121
122 RawPointerEventType::LeaveWindow => {
123 if (is_tip_event && !self.pointer_over_owner.get())
124 || (is_tip_owner_window_event && !self.pointer_over_tip.get())
125 {
126 if tip_popup_input_root.is_some() {
127 self.schedule_pending_close();
130 } else {
131 self.close_current_tip();
132 }
133 }
134 }
135
136 RawPointerEventType::LeftButtonDown
137 | RawPointerEventType::RightButtonDown
138 | RawPointerEventType::MiddleButtonDown
139 | RawPointerEventType::XButton1Down
140 | RawPointerEventType::XButton2Down => {
141 self.stop_timer();
143 if let Some(tip_control) = self.tip_control() {
144 tip_control.clear_value(ToolTip::is_open_property());
145 }
146 self.pointer_over_tip.set(false);
147 }
148
149 _ => {}
150 }
151 }
152
153 fn schedule_pending_close(&self) {
154 let control = self.tip_control();
155 let close_id = self.pending_close_id.get().wrapping_add(1);
156 self.pending_close_id.set(close_id);
157
158 let weak = self.this.clone();
160 Dispatcher::ui_thread().post_local(
161 move || {
162 let Some(this) = weak.upgrade() else { return };
163 if this.pending_close_id.get() == close_id && this.tip_control() == control {
164 this.close_current_tip();
165 }
166 },
167 DispatcherPriority::BACKGROUND,
168 );
169 }
170
171 fn cancel_pending_close(&self) {
172 self.pending_close_id.set(self.pending_close_id.get().wrapping_add(1));
173 }
174
175 fn close_current_tip(&self) {
176 self.stop_timer();
177 if let Some(tip_control) = self.tip_control() {
178 tip_control.clear_value(ToolTip::is_open_property());
179 }
180 self.set_tip_control(None);
181 self.pointer_over_tip.set(false);
182 self.pointer_over_owner.set(false);
183 }
184
185 fn service_enabled_changed(&self, args: &FerroPropertyChangedEventArgs<'_>) {
186 let Some(tip_control) = self.tip_control() else { return };
187 let sender = args.sender().downcast_ref::<Control>();
188
189 if sender.is_some_and(|sender| sender.to_ref() == tip_control) && !ToolTip::get_service_enabled(&tip_control) {
190 self.stop_timer();
191 }
192 }
193
194 fn tip_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
196 let control = e.sender().downcast_ref::<Control>().expect("the tooltip properties are attached to controls");
197 let (old_value, new_value) = e.get_old_and_new_value::<Option<BoxedValue>>();
198
199 let changed = match (&old_value, &new_value) {
200 (None, None) => false,
201 (Some(old_value), Some(new_value)) => !same_value(old_value, new_value),
202 _ => true,
203 };
204
205 if ToolTip::get_is_open(control) && changed && !new_value.as_ref().is_some_and(is_tool_tip) {
206 match new_value {
207 None => self.close(control),
208 Some(new_value) => {
209 if let Some(tip) = control.get_value(ToolTip::tool_tip_property()) {
210 tip.set_content(Some(new_value));
211 }
212 }
213 }
214 }
215 }
216
217 fn on_tip_control_changed(&self, old_value: Option<&Ref<Control>>, new_value: Option<&Ref<Control>>) {
218 self.stop_timer();
219
220 let mut closed_previous_tip = false; if let Some(old_value) = old_value {
223 if ToolTip::get_is_open(old_value) {
224 self.close(old_value);
225 closed_previous_tip = true;
226 }
227 }
228
229 if let Some(new_value) = new_value {
230 if !ToolTip::get_is_open(new_value) {
231 let between_show_delay = ToolTip::get_between_show_delay(new_value);
232
233 let closed_recently = || {
234 self.last_tip_close_time.get().is_some_and(|last_tip_close_time| {
235 Dispatcher::ui_thread().now().wrapping_sub(last_tip_close_time) <= i64::from(between_show_delay)
236 })
237 };
238
239 let show_delay = if between_show_delay >= 0 && (closed_previous_tip || closed_recently()) {
240 0
241 } else {
242 ToolTip::get_show_delay(new_value)
243 };
244
245 if show_delay == 0 {
246 self.open(new_value);
247 } else {
248 self.start_show_timer(show_delay, new_value);
249 }
250 }
251 }
252 }
253
254 fn tool_tip_closed(&self) {
255 self.last_tip_close_time.set(Some(Dispatcher::ui_thread().now()));
256 }
257
258 fn tool_tip_pointer_exited(&self, tool_tip: &ToolTip) {
259 if let Some(control) = tool_tip.adorned_control() {
262 if Some(&control) != self.tip_control().as_ref() {
263 self.close(&control);
264 }
265 }
266 }
267
268 fn start_show_timer(&self, show_delay: i32, control: &Ref<Control>) {
269 let Ok(show_delay) = u64::try_from(show_delay) else {
270 panic!("The show delay of a tooltip must not be negative.");
271 };
272
273 let timer = DispatcherTimer::new();
274 timer.set_interval(Duration::from_millis(show_delay));
275 timer.set_tag(Some(Control::boxed(control)));
276 let _ = timer.tick({
278 let weak = self.this.clone();
279 let control = control.downgrade();
280 move |_| {
281 let (Some(this), Some(control)) = (weak.upgrade(), control.upgrade()) else { return };
282 if this.timer.borrow().is_some() {
283 this.open(&control);
284 }
285 }
286 });
287 *self.timer.borrow_mut() = Some(timer.clone());
288 timer.start();
289 }
290
291 fn open(&self, control: &Ref<Control>) {
292 self.stop_timer();
293
294 if control.is_attached_to_visual_tree() {
295 ToolTip::set_is_open(control, true);
296
297 if ToolTip::get_is_open(control) {
299 if let Some(tool_tip) = control.get_value(ToolTip::tool_tip_property()) {
300 self.subscribe_to_tool_tip(&tool_tip);
301 }
302 }
303 }
304 }
305
306 fn subscribe_to_tool_tip(&self, tool_tip: &Ref<ToolTip>) {
309 let pointer_exited = tool_tip.add_handler(InputElement::pointer_exited_event(), {
310 let weak = self.this.clone();
311 let weak_tool_tip = tool_tip.downgrade();
312 move |_, _: &PointerEventArgs| {
313 if let (Some(this), Some(tool_tip)) = (weak.upgrade(), weak_tool_tip.upgrade()) {
314 this.tool_tip_pointer_exited(&tool_tip);
315 }
316 }
317 });
318
319 let closed: Rc<RefCell<Option<Rc<dyn IDisposable>>>> = Rc::new(RefCell::new(None));
320 let subscription = tool_tip.closed({
321 let weak = self.this.clone();
322 let closed = closed.clone();
323 move |tool_tip| {
324 if let Some(this) = weak.upgrade() {
325 this.tool_tip_closed();
326 }
327
328 let subscription = closed.borrow_mut().take();
329 if let Some(subscription) = subscription {
330 subscription.dispose();
331 }
332 tool_tip.remove_handler(InputElement::pointer_exited_event(), pointer_exited);
333 }
334 });
335 *closed.borrow_mut() = Some(subscription);
336 }
337
338 fn close(&self, control: &Control) {
339 ToolTip::set_is_open(control, false);
340 }
341
342 fn stop_timer(&self) {
343 let timer = self.timer.borrow_mut().take();
344 if let Some(timer) = timer {
345 timer.stop();
346 }
347 }
348}
349
350impl IToolTipService for ToolTipService {
351 fn update(&self, root: &Rc<dyn IInputRoot>, candidate_tool_tip_host: Option<Ref<Visual>>) {
352 let tip_control = self.tip_control();
353 let current_tool_tip =
354 tip_control.as_ref().and_then(|control| control.get_value(ToolTip::tool_tip_property()));
355
356 let tool_tip_input_root = current_tool_tip
357 .as_ref()
358 .and_then(|tool_tip| tool_tip.popup_host())
359 .and_then(|host| host.hosted_visual_tree_root())
360 .and_then(|hosted_root| hosted_root.get_input_root());
361 if tool_tip_input_root.is_some_and(|tool_tip_input_root| same_input_root(root, &tool_tip_input_root)) {
362 return;
364 }
365
366 let current_tool_tip_visual: Option<Ref<Visual>> = current_tool_tip.map(Ref::upcast);
367 let mut candidate_tool_tip_host = candidate_tool_tip_host;
368
369 while let Some(candidate) = candidate_tool_tip_host.clone() {
370 if Some(&candidate) == current_tool_tip_visual.as_ref() {
372 return;
373 }
374
375 if let Some(control) = candidate.downcast_ref::<Control>() {
376 if !ToolTip::get_service_enabled(control) {
377 return;
378 }
379
380 if ToolTip::get_tip(control).is_some()
381 && (control.is_effectively_enabled() || ToolTip::get_show_on_disabled(control))
382 {
383 break;
384 }
385 }
386
387 candidate_tool_tip_host = candidate.visual_parent();
388 }
389
390 let new_control = candidate_tool_tip_host.and_then(|candidate| candidate.cast::<Control>());
391
392 if new_control == tip_control {
393 return;
394 }
395
396 self.on_tip_control_changed(tip_control.as_ref(), new_control.as_ref());
397 self.set_tip_control(new_control.as_ref());
398 self.pointer_over_tip.set(false);
399 self.pointer_over_owner.set(false);
400 }
401
402 fn as_tool_tip_service(&self) -> Option<&ToolTipService> {
403 Some(self)
404 }
405}
406
407impl IDisposable for ToolTipService {
408 fn dispose(&self) {
409 ToolTipService::dispose(self)
410 }
411}
412
413fn root_element_visual(root: &Rc<dyn IInputRoot>) -> Option<Ref<Visual>> {
417 root.try_root_element().map(Ref::upcast)
418}
419
420fn same_input_root(a: &Rc<dyn IInputRoot>, b: &Rc<dyn IInputRoot>) -> bool {
422 std::ptr::addr_eq(Rc::as_ptr(a), Rc::as_ptr(b))
423}