@effindomv2/fui-rs 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/COMMERCIAL.md +7 -0
  2. package/Cargo.toml +34 -0
  3. package/LICENSE.md +9 -0
  4. package/README.md +88 -0
  5. package/package.json +54 -0
  6. package/scripts/build.sh +227 -0
  7. package/scripts/check-runtime-dependency.sh +41 -0
  8. package/scripts/framework-host-services.ts +40 -0
  9. package/scripts/generate-host-events.ts +17 -0
  10. package/scripts/generate-host-services.ts +28 -0
  11. package/scripts/hostgen/common.ts +73 -0
  12. package/scripts/hostgen/registry.ts +159 -0
  13. package/scripts/hostgen/rust-host-events.ts +171 -0
  14. package/scripts/hostgen/rust-host-services.ts +257 -0
  15. package/src/animation.rs +625 -0
  16. package/src/app.rs +324 -0
  17. package/src/assets.rs +572 -0
  18. package/src/bindings/mod.rs +1 -0
  19. package/src/bindings/ui.rs +705 -0
  20. package/src/bitmap.rs +317 -0
  21. package/src/bridge_callbacks.rs +242 -0
  22. package/src/context_menu_manager.rs +332 -0
  23. package/src/controls/anti_selection_area.rs +54 -0
  24. package/src/controls/button.rs +542 -0
  25. package/src/controls/checkbox.rs +372 -0
  26. package/src/controls/combobox.rs +1574 -0
  27. package/src/controls/context_menu.rs +1367 -0
  28. package/src/controls/control_template_set.rs +46 -0
  29. package/src/controls/control_tokens.rs +596 -0
  30. package/src/controls/dialog.rs +590 -0
  31. package/src/controls/dropdown.rs +1010 -0
  32. package/src/controls/form.rs +229 -0
  33. package/src/controls/internal/button_presenter.rs +142 -0
  34. package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
  35. package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
  36. package/src/controls/internal/dropdown_field_presenter.rs +269 -0
  37. package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
  38. package/src/controls/internal/mod.rs +13 -0
  39. package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
  40. package/src/controls/internal/pressable_labeled_control.rs +492 -0
  41. package/src/controls/internal/radio_indicator_presenter.rs +208 -0
  42. package/src/controls/internal/selectable_popup_list.rs +597 -0
  43. package/src/controls/internal/slider_presenter.rs +282 -0
  44. package/src/controls/internal/switch_indicator_presenter.rs +235 -0
  45. package/src/controls/internal/text_input_core.rs +1074 -0
  46. package/src/controls/internal/text_input_presenter.rs +108 -0
  47. package/src/controls/mod.rs +235 -0
  48. package/src/controls/nav_link.rs +395 -0
  49. package/src/controls/popup.rs +191 -0
  50. package/src/controls/progress_bar.rs +287 -0
  51. package/src/controls/radio_button.rs +348 -0
  52. package/src/controls/radio_group.rs +283 -0
  53. package/src/controls/selection_area.rs +82 -0
  54. package/src/controls/shared.rs +68 -0
  55. package/src/controls/slider.rs +701 -0
  56. package/src/controls/switch.rs +293 -0
  57. package/src/controls/templating.rs +49 -0
  58. package/src/controls/tests.rs +3905 -0
  59. package/src/controls/text_area.rs +207 -0
  60. package/src/controls/text_input.rs +192 -0
  61. package/src/debug.rs +146 -0
  62. package/src/drag_drop.rs +424 -0
  63. package/src/drag_gesture.rs +258 -0
  64. package/src/drawing.rs +385 -0
  65. package/src/event.rs +1603 -0
  66. package/src/external_drop.rs +467 -0
  67. package/src/fetch.rs +500 -0
  68. package/src/ffi.rs +3542 -0
  69. package/src/file.rs +1677 -0
  70. package/src/focus_adorner.rs +307 -0
  71. package/src/focus_visibility.rs +121 -0
  72. package/src/frame_scheduler.rs +159 -0
  73. package/src/frame_signal.rs +64 -0
  74. package/src/generated/ffi.rs +798 -0
  75. package/src/generated/framework_host_services.rs +74 -0
  76. package/src/generated/mod.rs +2 -0
  77. package/src/host_services.rs +162 -0
  78. package/src/image_sampling.rs +78 -0
  79. package/src/keyboard_scroll.rs +137 -0
  80. package/src/keyboard_scroll_tracker.rs +385 -0
  81. package/src/lib.rs +547 -0
  82. package/src/logger.rs +56 -0
  83. package/src/mobile_text_selection_toolbar.rs +1034 -0
  84. package/src/navigation.rs +48 -0
  85. package/src/node/core.rs +2210 -0
  86. package/src/node/custom_drawable.rs +149 -0
  87. package/src/node/flex_box.rs +874 -0
  88. package/src/node/grid.rs +304 -0
  89. package/src/node/helpers.rs +442 -0
  90. package/src/node/image.rs +506 -0
  91. package/src/node/mod.rs +315 -0
  92. package/src/node/scroll_bar.rs +845 -0
  93. package/src/node/scroll_box.rs +514 -0
  94. package/src/node/scroll_state.rs +121 -0
  95. package/src/node/scroll_view.rs +557 -0
  96. package/src/node/svg_node.rs +474 -0
  97. package/src/node/text_node.rs +633 -0
  98. package/src/node/virtual_list.rs +678 -0
  99. package/src/panic_hook.rs +36 -0
  100. package/src/persisted.rs +274 -0
  101. package/src/platform.rs +556 -0
  102. package/src/popup_presenter.rs +344 -0
  103. package/src/selection_handle_adorner.rs +838 -0
  104. package/src/signal.rs +134 -0
  105. package/src/text.rs +1065 -0
  106. package/src/theme.rs +571 -0
  107. package/src/timers.rs +106 -0
  108. package/src/tool_tip.rs +167 -0
  109. package/src/tool_tip_manager.rs +691 -0
  110. package/src/transitions.rs +41 -0
  111. package/src/typography.rs +520 -0
  112. package/src/viewport.rs +108 -0
  113. package/src/worker.rs +308 -0
  114. package/src/worker_job.rs +82 -0
  115. package/src/worker_runtime.rs +172 -0
@@ -0,0 +1,691 @@
1
+ use crate::app::Application;
2
+ use crate::bindings::ui;
3
+ use crate::ffi::PositionType;
4
+ use crate::focus_visibility;
5
+ use crate::generated::framework_host_services::fui_now_ms;
6
+ use crate::node::{portal, text, FlexBox, Node, NodeHandle, NodeRef, TextNode, WeakNodeRef};
7
+ use crate::theme::current_theme;
8
+ use crate::timers;
9
+ use crate::{FlexDirection, PopupPlacement, PopupPresenter, ToolTip, Unit};
10
+ use std::cell::RefCell;
11
+
12
+ const SHOW_TIMER_ID: u32 = 0x5454_5001;
13
+ const HIDE_TIMER_ID: u32 = 0x5454_5002;
14
+ const MIN_TOOLTIP_SURFACE_SIZE: f32 = 1.0;
15
+
16
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
17
+ enum ToolTipAnchorKind {
18
+ None = 0,
19
+ Owner = 1,
20
+ Pointer = 2,
21
+ }
22
+
23
+ struct ToolTipManagerState {
24
+ host_root: Option<FlexBox>,
25
+ panel_node: Option<FlexBox>,
26
+ label_node: Option<TextNode>,
27
+ presenter: Option<PopupPresenter>,
28
+ active_owner: Option<WeakNodeRef>,
29
+ active_owner_handle: Option<NodeHandle>,
30
+ active_tool_tip: Option<ToolTip>,
31
+ pending_owner: Option<WeakNodeRef>,
32
+ pending_owner_handle: Option<NodeHandle>,
33
+ pending_tool_tip: Option<ToolTip>,
34
+ hovered_owner: Option<WeakNodeRef>,
35
+ hovered_owner_handle: Option<NodeHandle>,
36
+ hovered_tool_tip: Option<ToolTip>,
37
+ suppressed_hover_owner_handle: Option<NodeHandle>,
38
+ focused_owner: Option<WeakNodeRef>,
39
+ focused_owner_handle: Option<NodeHandle>,
40
+ focused_tool_tip: Option<ToolTip>,
41
+ quick_show_until_ms: f64,
42
+ hovered_pointer_x: f32,
43
+ hovered_pointer_y: f32,
44
+ pending_anchor_kind: ToolTipAnchorKind,
45
+ pending_popup_x: f32,
46
+ pending_popup_y: f32,
47
+ active_anchor_kind: ToolTipAnchorKind,
48
+ active_anchor_x: f32,
49
+ active_anchor_y: f32,
50
+ active_anchor_width: f32,
51
+ active_anchor_height: f32,
52
+ active_popup_x: f32,
53
+ active_popup_y: f32,
54
+ focus_visibility_subscription: Option<crate::signal::SubscriptionGuard>,
55
+ }
56
+
57
+ impl Default for ToolTipManagerState {
58
+ fn default() -> Self {
59
+ Self {
60
+ host_root: None,
61
+ panel_node: None,
62
+ label_node: None,
63
+ presenter: None,
64
+ active_owner: None,
65
+ active_owner_handle: None,
66
+ active_tool_tip: None,
67
+ pending_owner: None,
68
+ pending_owner_handle: None,
69
+ pending_tool_tip: None,
70
+ hovered_owner: None,
71
+ hovered_owner_handle: None,
72
+ hovered_tool_tip: None,
73
+ suppressed_hover_owner_handle: None,
74
+ focused_owner: None,
75
+ focused_owner_handle: None,
76
+ focused_tool_tip: None,
77
+ quick_show_until_ms: -1.0,
78
+ hovered_pointer_x: f32::NAN,
79
+ hovered_pointer_y: f32::NAN,
80
+ pending_anchor_kind: ToolTipAnchorKind::None,
81
+ pending_popup_x: f32::NAN,
82
+ pending_popup_y: f32::NAN,
83
+ active_anchor_kind: ToolTipAnchorKind::None,
84
+ active_anchor_x: f32::NAN,
85
+ active_anchor_y: f32::NAN,
86
+ active_anchor_width: f32::NAN,
87
+ active_anchor_height: f32::NAN,
88
+ active_popup_x: f32::NAN,
89
+ active_popup_y: f32::NAN,
90
+ focus_visibility_subscription: None,
91
+ }
92
+ }
93
+ }
94
+
95
+ thread_local! {
96
+ static TOOL_TIP_MANAGER: RefCell<ToolTipManagerState> = RefCell::new(ToolTipManagerState::default());
97
+ }
98
+
99
+ pub(crate) struct ToolTipManager;
100
+
101
+ impl ToolTipManager {
102
+ pub(crate) fn create_default_host() -> FlexBox {
103
+ let (host_root, needs_focus_visibility_subscription) = TOOL_TIP_MANAGER.with(|slot| {
104
+ let mut state = slot.borrow_mut();
105
+ if let Some(host_root) = state.host_root.as_ref() {
106
+ return (
107
+ host_root.clone(),
108
+ state.focus_visibility_subscription.is_none(),
109
+ );
110
+ }
111
+ let label_node = text("");
112
+ let panel_node = FlexBox::default();
113
+ panel_node
114
+ .position_type(PositionType::Absolute)
115
+ .flex_direction(FlexDirection::Column)
116
+ .child(&label_node);
117
+ let host_root = portal();
118
+ host_root
119
+ .position_type(PositionType::Absolute)
120
+ .position(0.0, 0.0)
121
+ .width(100.0, Unit::Percent)
122
+ .height(100.0, Unit::Percent);
123
+ let presenter = PopupPresenter::new_with_semantic_scope(
124
+ host_root.clone(),
125
+ panel_node.clone(),
126
+ None,
127
+ );
128
+ state.host_root = Some(host_root.clone());
129
+ state.panel_node = Some(panel_node);
130
+ state.label_node = Some(label_node);
131
+ state.presenter = Some(presenter);
132
+ (host_root, state.focus_visibility_subscription.is_none())
133
+ });
134
+ if needs_focus_visibility_subscription {
135
+ let guard = focus_visibility::subscribe(|_| ToolTipManager::activate_best_candidate());
136
+ TOOL_TIP_MANAGER.with(|slot| {
137
+ let mut state = slot.borrow_mut();
138
+ if state.focus_visibility_subscription.is_none() {
139
+ state.focus_visibility_subscription = Some(guard);
140
+ }
141
+ });
142
+ }
143
+ host_root
144
+ }
145
+
146
+ pub(crate) fn clear() {
147
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
148
+ TOOL_TIP_MANAGER.with(|slot| {
149
+ let mut state = slot.borrow_mut();
150
+ state.pending_owner = None;
151
+ state.pending_owner_handle = None;
152
+ state.pending_tool_tip = None;
153
+ state.hovered_owner = None;
154
+ state.hovered_owner_handle = None;
155
+ state.hovered_tool_tip = None;
156
+ state.suppressed_hover_owner_handle = None;
157
+ state.hovered_pointer_x = f32::NAN;
158
+ state.hovered_pointer_y = f32::NAN;
159
+ state.focused_owner = None;
160
+ state.focused_owner_handle = None;
161
+ state.focused_tool_tip = None;
162
+ state.pending_anchor_kind = ToolTipAnchorKind::None;
163
+ state.pending_popup_x = f32::NAN;
164
+ state.pending_popup_y = f32::NAN;
165
+ state.quick_show_until_ms = -1.0;
166
+ });
167
+ Self::hide_current();
168
+ }
169
+
170
+ pub(crate) fn handle_tool_tip_changed(owner: &NodeRef, tool_tip: Option<ToolTip>) {
171
+ if tool_tip.is_none() {
172
+ Self::clear_owner(owner.handle());
173
+ Self::activate_best_candidate();
174
+ return;
175
+ }
176
+ let owner_handle = owner.handle();
177
+ let show_active_now = TOOL_TIP_MANAGER.with(|slot| {
178
+ let mut state = slot.borrow_mut();
179
+ if state.hovered_owner_handle == Some(owner_handle) {
180
+ state.hovered_tool_tip = tool_tip.clone();
181
+ }
182
+ if state.focused_owner_handle == Some(owner_handle) {
183
+ state.focused_tool_tip = tool_tip.clone();
184
+ }
185
+ let show_active_now = state.active_owner_handle == Some(owner_handle);
186
+ if show_active_now {
187
+ state.active_tool_tip = tool_tip.clone();
188
+ }
189
+ if state.pending_owner_handle == Some(owner_handle) {
190
+ state.pending_tool_tip = tool_tip;
191
+ }
192
+ show_active_now
193
+ });
194
+ if show_active_now {
195
+ if let Some(tool_tip) = owner.tool_tip_for_routing() {
196
+ Self::show_now(owner_handle, tool_tip, true, ToolTipAnchorKind::Owner);
197
+ }
198
+ return;
199
+ }
200
+ Self::activate_best_candidate();
201
+ }
202
+
203
+ pub(crate) fn handle_pointer_enter(owner: &NodeRef, tool_tip: Option<ToolTip>, x: f32, y: f32) {
204
+ let Some(tool_tip) = tool_tip else {
205
+ return;
206
+ };
207
+ if tool_tip.content_text().is_empty() {
208
+ return;
209
+ }
210
+ TOOL_TIP_MANAGER.with(|slot| {
211
+ let mut state = slot.borrow_mut();
212
+ if state.suppressed_hover_owner_handle == Some(owner.handle()) {
213
+ state.suppressed_hover_owner_handle = None;
214
+ }
215
+ state.hovered_owner = Some(owner.downgrade());
216
+ state.hovered_owner_handle = Some(owner.handle());
217
+ state.hovered_tool_tip = Some(tool_tip);
218
+ state.hovered_pointer_x = x;
219
+ state.hovered_pointer_y = y;
220
+ });
221
+ Self::activate_best_candidate();
222
+ }
223
+
224
+ pub(crate) fn handle_pointer_move(owner: &NodeRef, x: f32, y: f32) {
225
+ TOOL_TIP_MANAGER.with(|slot| {
226
+ let mut state = slot.borrow_mut();
227
+ if state.hovered_owner_handle != Some(owner.handle()) {
228
+ return;
229
+ }
230
+ state.hovered_pointer_x = x;
231
+ state.hovered_pointer_y = y;
232
+ if state.pending_owner_handle == Some(owner.handle())
233
+ && state.pending_anchor_kind == ToolTipAnchorKind::Pointer
234
+ {
235
+ state.pending_popup_x = x;
236
+ state.pending_popup_y = y;
237
+ }
238
+ });
239
+ }
240
+
241
+ pub(crate) fn handle_pointer_leave(owner: &NodeRef) {
242
+ TOOL_TIP_MANAGER.with(|slot| {
243
+ let mut state = slot.borrow_mut();
244
+ if state.hovered_owner_handle == Some(owner.handle()) {
245
+ state.hovered_owner = None;
246
+ state.hovered_owner_handle = None;
247
+ state.hovered_tool_tip = None;
248
+ state.hovered_pointer_x = f32::NAN;
249
+ state.hovered_pointer_y = f32::NAN;
250
+ }
251
+ if state.suppressed_hover_owner_handle == Some(owner.handle()) {
252
+ state.suppressed_hover_owner_handle = None;
253
+ }
254
+ });
255
+ Self::activate_best_candidate();
256
+ }
257
+
258
+ pub(crate) fn handle_pointer_down(owner: &NodeRef) {
259
+ let should_hide = TOOL_TIP_MANAGER.with(|slot| {
260
+ let state = slot.borrow();
261
+ state.active_owner_handle == Some(owner.handle())
262
+ || state.pending_owner_handle == Some(owner.handle())
263
+ });
264
+ if !should_hide {
265
+ return;
266
+ }
267
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
268
+ Self::hide_current();
269
+ TOOL_TIP_MANAGER.with(|slot| {
270
+ let mut state = slot.borrow_mut();
271
+ state.pending_owner = None;
272
+ state.pending_owner_handle = None;
273
+ state.pending_tool_tip = None;
274
+ state.pending_anchor_kind = ToolTipAnchorKind::None;
275
+ state.pending_popup_x = f32::NAN;
276
+ state.pending_popup_y = f32::NAN;
277
+ });
278
+ }
279
+
280
+ pub(crate) fn handle_focus_changed(owner: &NodeRef, tool_tip: Option<ToolTip>, focused: bool) {
281
+ TOOL_TIP_MANAGER.with(|slot| {
282
+ let mut state = slot.borrow_mut();
283
+ if focused {
284
+ if let Some(tool_tip) = tool_tip {
285
+ if tool_tip.opens_on_focus() && !tool_tip.content_text().is_empty() {
286
+ state.focused_owner = Some(owner.downgrade());
287
+ state.focused_owner_handle = Some(owner.handle());
288
+ state.focused_tool_tip = Some(tool_tip);
289
+ }
290
+ }
291
+ } else if state.focused_owner_handle == Some(owner.handle()) {
292
+ state.focused_owner = None;
293
+ state.focused_owner_handle = None;
294
+ state.focused_tool_tip = None;
295
+ }
296
+ });
297
+ Self::activate_best_candidate();
298
+ }
299
+
300
+ pub(crate) fn handle_owner_destroyed(handle: NodeHandle) {
301
+ Self::clear_owner(handle);
302
+ Self::activate_best_candidate();
303
+ }
304
+
305
+ pub(crate) fn handle_scroll() {
306
+ let should_hide = TOOL_TIP_MANAGER.with(|slot| {
307
+ let state = slot.borrow();
308
+ state.active_anchor_kind == ToolTipAnchorKind::Pointer
309
+ && state.active_owner_handle.is_some()
310
+ });
311
+ if !should_hide {
312
+ return;
313
+ }
314
+ TOOL_TIP_MANAGER.with(|slot| {
315
+ let mut state = slot.borrow_mut();
316
+ state.suppressed_hover_owner_handle = state.active_owner_handle;
317
+ if state.hovered_owner_handle == state.active_owner_handle {
318
+ state.hovered_owner = None;
319
+ state.hovered_owner_handle = None;
320
+ state.hovered_tool_tip = None;
321
+ state.hovered_pointer_x = f32::NAN;
322
+ state.hovered_pointer_y = f32::NAN;
323
+ }
324
+ state.pending_owner = None;
325
+ state.pending_owner_handle = None;
326
+ state.pending_tool_tip = None;
327
+ state.pending_anchor_kind = ToolTipAnchorKind::None;
328
+ state.pending_popup_x = f32::NAN;
329
+ state.pending_popup_y = f32::NAN;
330
+ });
331
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
332
+ Self::hide_current();
333
+ Self::activate_best_candidate();
334
+ }
335
+
336
+ pub(crate) fn commit_pending_show() {
337
+ let (owner, tool_tip, anchor_kind) = TOOL_TIP_MANAGER.with(|slot| {
338
+ let mut state = slot.borrow_mut();
339
+ let owner = state.pending_owner.clone();
340
+ let tool_tip = state.pending_tool_tip.clone();
341
+ let anchor_kind = state.pending_anchor_kind;
342
+ state.pending_owner = None;
343
+ state.pending_owner_handle = None;
344
+ state.pending_tool_tip = None;
345
+ state.pending_anchor_kind = ToolTipAnchorKind::None;
346
+ (owner, tool_tip, anchor_kind)
347
+ });
348
+ let Some(tool_tip) = tool_tip else {
349
+ return;
350
+ };
351
+ let Some(owner) = owner.and_then(|weak| weak.upgrade()) else {
352
+ return;
353
+ };
354
+ Self::show_now(owner.handle(), tool_tip, false, anchor_kind);
355
+ }
356
+
357
+ fn clear_owner(handle: NodeHandle) {
358
+ TOOL_TIP_MANAGER.with(|slot| {
359
+ let mut state = slot.borrow_mut();
360
+ if state.hovered_owner_handle == Some(handle) {
361
+ state.hovered_owner = None;
362
+ state.hovered_owner_handle = None;
363
+ state.hovered_tool_tip = None;
364
+ state.hovered_pointer_x = f32::NAN;
365
+ state.hovered_pointer_y = f32::NAN;
366
+ }
367
+ if state.suppressed_hover_owner_handle == Some(handle) {
368
+ state.suppressed_hover_owner_handle = None;
369
+ }
370
+ if state.focused_owner_handle == Some(handle) {
371
+ state.focused_owner = None;
372
+ state.focused_owner_handle = None;
373
+ state.focused_tool_tip = None;
374
+ }
375
+ if state.pending_owner_handle == Some(handle) {
376
+ state.pending_owner = None;
377
+ state.pending_owner_handle = None;
378
+ state.pending_tool_tip = None;
379
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
380
+ }
381
+ });
382
+ let is_active =
383
+ TOOL_TIP_MANAGER.with(|slot| slot.borrow().active_owner_handle == Some(handle));
384
+ if is_active {
385
+ Self::hide_current();
386
+ }
387
+ }
388
+
389
+ fn activate_best_candidate() {
390
+ let (candidate_owner, candidate_tool_tip, candidate_anchor_kind, same_as_active) =
391
+ TOOL_TIP_MANAGER.with(|slot| {
392
+ let state = slot.borrow();
393
+ let hovered_candidate =
394
+ if state.hovered_owner_handle != state.suppressed_hover_owner_handle {
395
+ state.hovered_owner.clone()
396
+ } else {
397
+ None
398
+ };
399
+ let candidate_owner = if hovered_candidate.is_some() {
400
+ hovered_candidate
401
+ } else if focus_visibility::keyboard_focus_visible() {
402
+ state.focused_owner.clone()
403
+ } else {
404
+ None
405
+ };
406
+ let candidate_tool_tip = if state.hovered_owner_handle
407
+ != state.suppressed_hover_owner_handle
408
+ && state.hovered_owner.is_some()
409
+ {
410
+ state.hovered_tool_tip.clone()
411
+ } else if focus_visibility::keyboard_focus_visible() {
412
+ state.focused_tool_tip.clone()
413
+ } else {
414
+ None
415
+ };
416
+ let candidate_anchor_kind = if state.hovered_owner_handle
417
+ != state.suppressed_hover_owner_handle
418
+ && state.hovered_owner.is_some()
419
+ {
420
+ ToolTipAnchorKind::Pointer
421
+ } else {
422
+ ToolTipAnchorKind::Owner
423
+ };
424
+ let candidate_handle = candidate_owner
425
+ .as_ref()
426
+ .and_then(|weak| weak.upgrade())
427
+ .map(|owner| owner.handle());
428
+ let same_as_active = state.active_owner_handle == candidate_handle
429
+ && state.active_tool_tip == candidate_tool_tip;
430
+ (
431
+ candidate_owner,
432
+ candidate_tool_tip,
433
+ candidate_anchor_kind,
434
+ same_as_active,
435
+ )
436
+ });
437
+
438
+ let Some(tool_tip) = candidate_tool_tip else {
439
+ TOOL_TIP_MANAGER.with(|slot| {
440
+ let mut state = slot.borrow_mut();
441
+ state.pending_owner = None;
442
+ state.pending_owner_handle = None;
443
+ state.pending_tool_tip = None;
444
+ state.pending_anchor_kind = ToolTipAnchorKind::None;
445
+ state.pending_popup_x = f32::NAN;
446
+ state.pending_popup_y = f32::NAN;
447
+ });
448
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
449
+ Self::hide_current();
450
+ return;
451
+ };
452
+ if tool_tip.content_text().is_empty() {
453
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
454
+ Self::hide_current();
455
+ return;
456
+ }
457
+ if same_as_active {
458
+ return;
459
+ }
460
+ let Some(owner) = candidate_owner.and_then(|weak| weak.upgrade()) else {
461
+ return;
462
+ };
463
+ Self::request_show(owner, tool_tip, candidate_anchor_kind);
464
+ }
465
+
466
+ fn request_show(owner: NodeRef, tool_tip: ToolTip, anchor_kind: ToolTipAnchorKind) {
467
+ TOOL_TIP_MANAGER.with(|slot| {
468
+ let mut state = slot.borrow_mut();
469
+ state.pending_owner = Some(owner.downgrade());
470
+ state.pending_owner_handle = Some(owner.handle());
471
+ state.pending_tool_tip = Some(tool_tip.clone());
472
+ state.pending_anchor_kind = anchor_kind;
473
+ state.pending_popup_x = if anchor_kind == ToolTipAnchorKind::Pointer {
474
+ state.hovered_pointer_x
475
+ } else {
476
+ f32::NAN
477
+ };
478
+ state.pending_popup_y = if anchor_kind == ToolTipAnchorKind::Pointer {
479
+ state.hovered_pointer_y
480
+ } else {
481
+ f32::NAN
482
+ };
483
+ });
484
+ timers::cancel_internal_timer(HIDE_TIMER_ID);
485
+ let now = fui_now_ms();
486
+ let delay_ms = TOOL_TIP_MANAGER.with(|slot| {
487
+ let state = slot.borrow();
488
+ if now <= state.quick_show_until_ms {
489
+ 0
490
+ } else {
491
+ tool_tip.initial_show_delay_ms()
492
+ }
493
+ });
494
+ timers::cancel_internal_timer(SHOW_TIMER_ID);
495
+ if delay_ms <= 0 {
496
+ Self::commit_pending_show();
497
+ return;
498
+ }
499
+ timers::schedule_internal_timer(SHOW_TIMER_ID, delay_ms, || {
500
+ ToolTipManager::commit_pending_show();
501
+ });
502
+ }
503
+
504
+ fn show_now(
505
+ owner_handle: NodeHandle,
506
+ tool_tip: ToolTip,
507
+ preserve_current_popup_anchor: bool,
508
+ anchor_kind: ToolTipAnchorKind,
509
+ ) {
510
+ let Some(owner) = Application::resolve_mounted_node(owner_handle) else {
511
+ return;
512
+ };
513
+ let (presenter, panel_node, label_node, host_root) = TOOL_TIP_MANAGER.with(|slot| {
514
+ let state = slot.borrow();
515
+ (
516
+ state.presenter.clone(),
517
+ state.panel_node.clone(),
518
+ state.label_node.clone(),
519
+ state.host_root.clone(),
520
+ )
521
+ });
522
+ let (Some(presenter), Some(panel_node), Some(label_node), Some(host_root)) =
523
+ (presenter, panel_node, label_node, host_root)
524
+ else {
525
+ return;
526
+ };
527
+ if owner.handle() == NodeHandle::INVALID || host_root.handle() == NodeHandle::INVALID {
528
+ return;
529
+ }
530
+
531
+ owner.append_child_ref(&host_root.node_ref());
532
+
533
+ let theme = current_theme().tool_tip;
534
+ let panel_background = if tool_tip.has_panel_color_override() {
535
+ tool_tip.panel_background_color()
536
+ } else {
537
+ theme.panel_background
538
+ };
539
+ let text_color = if tool_tip.has_text_color_override() {
540
+ tool_tip.tooltip_text_color()
541
+ } else {
542
+ theme.text_color
543
+ };
544
+ label_node
545
+ .text(tool_tip.content_text())
546
+ .font_family(theme.font_family.clone())
547
+ .font_size(theme.font_size)
548
+ .text_color(text_color)
549
+ .wrapping(true);
550
+ panel_node
551
+ .padding(
552
+ theme.padding_left,
553
+ theme.padding_top,
554
+ theme.padding_right,
555
+ theme.padding_bottom,
556
+ )
557
+ .corner_radius(theme.panel_corner_radius)
558
+ .border(1.0, theme.panel_border_color)
559
+ .drop_shadow(
560
+ theme.panel_shadow_color,
561
+ 0.0,
562
+ theme.shadow_offset_y,
563
+ theme.shadow_blur,
564
+ theme.shadow_spread,
565
+ )
566
+ .bg_color(panel_background)
567
+ .width(0.0, Unit::Auto)
568
+ .height(0.0, Unit::Auto)
569
+ .max_width(theme.max_width, Unit::Pixel);
570
+ presenter.placement(tool_tip.popup_placement());
571
+ presenter.anchor_gap(8.0);
572
+
573
+ TOOL_TIP_MANAGER.with(|slot| {
574
+ let mut state = slot.borrow_mut();
575
+ if !preserve_current_popup_anchor || state.active_owner_handle != Some(owner_handle) {
576
+ if anchor_kind == ToolTipAnchorKind::Pointer
577
+ && !state.pending_popup_x.is_nan()
578
+ && !state.pending_popup_y.is_nan()
579
+ {
580
+ state.active_anchor_kind = ToolTipAnchorKind::Pointer;
581
+ state.active_popup_x = state.pending_popup_x + tool_tip.horizontal_offset_px();
582
+ state.active_popup_y = state.pending_popup_y + tool_tip.vertical_offset_px();
583
+ state.active_anchor_x = f32::NAN;
584
+ state.active_anchor_y = f32::NAN;
585
+ state.active_anchor_width = f32::NAN;
586
+ state.active_anchor_height = f32::NAN;
587
+ } else {
588
+ let bounds = ui::get_bounds(owner_handle.raw()).unwrap_or([0.0, 0.0, 1.0, 1.0]);
589
+ state.active_anchor_kind = ToolTipAnchorKind::Owner;
590
+ state.active_anchor_x = bounds[0] + tool_tip.horizontal_offset_px();
591
+ state.active_anchor_y = bounds[1] + tool_tip.vertical_offset_px();
592
+ state.active_anchor_width = bounds[2];
593
+ state.active_anchor_height = bounds[3];
594
+ state.active_popup_x = f32::NAN;
595
+ state.active_popup_y = f32::NAN;
596
+ }
597
+ }
598
+ });
599
+
600
+ Self::show_at_resolved_anchor(
601
+ &presenter,
602
+ tool_tip.popup_placement(),
603
+ MIN_TOOLTIP_SURFACE_SIZE,
604
+ MIN_TOOLTIP_SURFACE_SIZE,
605
+ );
606
+ Application::flush_renders();
607
+ let measured_bounds = if panel_node.handle() != NodeHandle::INVALID {
608
+ ui::get_bounds(panel_node.handle().raw())
609
+ } else {
610
+ None
611
+ };
612
+ let measured_width = measured_bounds
613
+ .map(|bounds| bounds[2].max(MIN_TOOLTIP_SURFACE_SIZE))
614
+ .unwrap_or(MIN_TOOLTIP_SURFACE_SIZE);
615
+ let measured_height = measured_bounds
616
+ .map(|bounds| bounds[3].max(MIN_TOOLTIP_SURFACE_SIZE))
617
+ .unwrap_or(MIN_TOOLTIP_SURFACE_SIZE);
618
+ Self::show_at_resolved_anchor(
619
+ &presenter,
620
+ tool_tip.popup_placement(),
621
+ measured_width,
622
+ measured_height,
623
+ );
624
+ Application::flush_renders();
625
+
626
+ TOOL_TIP_MANAGER.with(|slot| {
627
+ let mut state = slot.borrow_mut();
628
+ state.active_owner = Some(owner.downgrade());
629
+ state.active_owner_handle = Some(owner_handle);
630
+ state.active_tool_tip = Some(tool_tip.clone());
631
+ state.quick_show_until_ms = fui_now_ms() + f64::from(tool_tip.between_show_delay_ms());
632
+ });
633
+ timers::cancel_internal_timer(HIDE_TIMER_ID);
634
+ if tool_tip.show_duration_ms() > 0 {
635
+ timers::schedule_internal_timer(HIDE_TIMER_ID, tool_tip.show_duration_ms(), || {
636
+ ToolTipManager::hide_current();
637
+ ToolTipManager::activate_best_candidate();
638
+ });
639
+ }
640
+ }
641
+
642
+ fn show_at_resolved_anchor(
643
+ presenter: &PopupPresenter,
644
+ placement: PopupPlacement,
645
+ width: f32,
646
+ height: f32,
647
+ ) {
648
+ TOOL_TIP_MANAGER.with(|slot| {
649
+ let state = slot.borrow();
650
+ if state.active_anchor_kind == ToolTipAnchorKind::Pointer
651
+ && !state.active_popup_x.is_nan()
652
+ && !state.active_popup_y.is_nan()
653
+ {
654
+ presenter.show_at_point(state.active_popup_x, state.active_popup_y, width, height);
655
+ } else {
656
+ presenter.show_anchored_with_placement(
657
+ state.active_anchor_x,
658
+ state.active_anchor_y,
659
+ state.active_anchor_width,
660
+ state.active_anchor_height,
661
+ width,
662
+ height,
663
+ placement,
664
+ );
665
+ }
666
+ });
667
+ }
668
+
669
+ fn hide_current() {
670
+ timers::cancel_internal_timer(HIDE_TIMER_ID);
671
+ TOOL_TIP_MANAGER.with(|slot| {
672
+ let mut state = slot.borrow_mut();
673
+ if let Some(presenter) = state.presenter.as_ref() {
674
+ presenter.hide();
675
+ }
676
+ if let Some(host_root) = state.host_root.as_ref() {
677
+ host_root.node_ref().detach_from_parent();
678
+ }
679
+ state.active_owner = None;
680
+ state.active_owner_handle = None;
681
+ state.active_tool_tip = None;
682
+ state.active_anchor_kind = ToolTipAnchorKind::None;
683
+ state.active_anchor_x = f32::NAN;
684
+ state.active_anchor_y = f32::NAN;
685
+ state.active_anchor_width = f32::NAN;
686
+ state.active_anchor_height = f32::NAN;
687
+ state.active_popup_x = f32::NAN;
688
+ state.active_popup_y = f32::NAN;
689
+ });
690
+ }
691
+ }