@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,1034 @@
1
+ use crate::bindings::ui;
2
+ use crate::controls::{run_context_menu_action, ContextMenuAction, MenuItem};
3
+ use crate::ffi::{
4
+ AlignItems, FlexDirection, JustifyContent, PositionType, SemanticRole, TextOverflow, Unit,
5
+ Visibility,
6
+ };
7
+ use crate::node::FlexBoxSurface;
8
+ use crate::node::{
9
+ flex_box, portal, text, FlexBox, Node, NodeHandle, NodeRef, ScrollBox, TextNode,
10
+ };
11
+ use crate::theme::current_theme;
12
+ use std::cell::RefCell;
13
+
14
+ const TOOLBAR_MARGIN: f32 = 8.0;
15
+ const EDGE_MARGIN: f32 = 8.0;
16
+ const READONLY_BUTTON_WIDTH: f32 = 96.0;
17
+ const EDITABLE_BUTTON_WIDTH: f32 = 72.0;
18
+ const OVERFLOW_BUTTON_WIDTH: f32 = 44.0;
19
+ const VERTICAL_MENU_WIDTH: f32 = 168.0;
20
+ const VERTICAL_MENU_MAX_HEIGHT: f32 = 184.0;
21
+ const TOOLBAR_HORIZONTAL_PADDING: f32 = 4.0;
22
+ const MAX_HORIZONTAL_ACTIONS: usize = 3;
23
+ const OVERFLOW_SLOT: i32 = -1;
24
+ const BACK_SLOT: i32 = -2;
25
+ const OVERFLOW_LABEL: &str = "More";
26
+
27
+ #[derive(Clone)]
28
+ struct ToolbarButton {
29
+ root: FlexBox,
30
+ label_node: Option<TextNode>,
31
+ dot_nodes: Vec<FlexBox>,
32
+ }
33
+
34
+ struct State {
35
+ host_root: Option<FlexBox>,
36
+ panel: Option<FlexBox>,
37
+ overflow_panel: Option<FlexBox>,
38
+ overflow_scroll_box: Option<ScrollBox>,
39
+ overflow_content: Option<FlexBox>,
40
+ buttons: Vec<ToolbarButton>,
41
+ overflow_buttons: Vec<ToolbarButton>,
42
+ separators: Vec<FlexBox>,
43
+ overflow_separators: Vec<FlexBox>,
44
+ active_items: Vec<MenuItem>,
45
+ active_handle: NodeHandle,
46
+ active_start: u32,
47
+ active_end: u32,
48
+ active_cross_selection_text: String,
49
+ active_cross_selection_select_all_target: NodeHandle,
50
+ pending_cross_selection_text_handle: NodeHandle,
51
+ hidden_for_handle_drag: bool,
52
+ overflow_visible: bool,
53
+ horizontal_item_count: usize,
54
+ last_panel_x: f32,
55
+ last_panel_y: f32,
56
+ last_panel_width: f32,
57
+ }
58
+
59
+ impl State {
60
+ fn new() -> Self {
61
+ Self {
62
+ host_root: None,
63
+ panel: None,
64
+ overflow_panel: None,
65
+ overflow_scroll_box: None,
66
+ overflow_content: None,
67
+ buttons: Vec::new(),
68
+ overflow_buttons: Vec::new(),
69
+ separators: Vec::new(),
70
+ overflow_separators: Vec::new(),
71
+ active_items: Vec::new(),
72
+ active_handle: NodeHandle::INVALID,
73
+ active_start: 0,
74
+ active_end: 0,
75
+ active_cross_selection_text: String::new(),
76
+ active_cross_selection_select_all_target: NodeHandle::INVALID,
77
+ pending_cross_selection_text_handle: NodeHandle::INVALID,
78
+ hidden_for_handle_drag: false,
79
+ overflow_visible: false,
80
+ horizontal_item_count: 0,
81
+ last_panel_x: EDGE_MARGIN,
82
+ last_panel_y: EDGE_MARGIN,
83
+ last_panel_width: 0.0,
84
+ }
85
+ }
86
+ }
87
+
88
+ thread_local! {
89
+ static STATE: RefCell<State> = RefCell::new(State::new());
90
+ }
91
+
92
+ impl ToolbarButton {
93
+ fn new(label: &str, width: f32, slot: i32) -> Self {
94
+ let child = if slot == OVERFLOW_SLOT {
95
+ ToolbarButtonChild::Overflow(create_overflow_icon())
96
+ } else {
97
+ ToolbarButtonChild::Label(create_label(label, slot))
98
+ };
99
+ let (child_node, label_node, dot_nodes) = match child {
100
+ ToolbarButtonChild::Label(node) => (node.node_ref(), Some(node), Vec::new()),
101
+ ToolbarButtonChild::Overflow((icon, dots)) => (icon.node_ref(), None, dots),
102
+ };
103
+ let theme = current_theme();
104
+ let root = flex_box();
105
+ root.width(width, Unit::Pixel)
106
+ .height(theme.context_menu.item.height, Unit::Pixel)
107
+ .align_items(AlignItems::Center)
108
+ .justify_content(JustifyContent::Center)
109
+ .padding(
110
+ theme.context_menu.item.padding_left,
111
+ theme.context_menu.item.padding_top,
112
+ theme.context_menu.item.padding_right,
113
+ theme.context_menu.item.padding_bottom,
114
+ )
115
+ .corner_radius(theme.context_menu.item.corner_radius)
116
+ .semantic_role(SemanticRole::Button)
117
+ .semantic_label(label)
118
+ .interactive(true)
119
+ .preserve_selection_on_pointer_down(true)
120
+ .on_pointer_up(move |event| {
121
+ activate_toolbar_slot(slot);
122
+ event.handled = true;
123
+ })
124
+ .on_click(move |_| activate_toolbar_slot(slot));
125
+ append_child_ref(&root, &child_node);
126
+ Self {
127
+ root,
128
+ label_node,
129
+ dot_nodes,
130
+ }
131
+ }
132
+
133
+ fn set_label(&self, label: &str) {
134
+ if let Some(label_node) = self.label_node.as_ref() {
135
+ label_node.text(label);
136
+ }
137
+ self.root.semantic_label(label.to_owned());
138
+ }
139
+
140
+ fn apply_style(&self) {
141
+ let theme = current_theme();
142
+ self.root
143
+ .height(theme.context_menu.item.height, Unit::Pixel)
144
+ .padding(
145
+ theme.context_menu.item.padding_left,
146
+ theme.context_menu.item.padding_top,
147
+ theme.context_menu.item.padding_right,
148
+ theme.context_menu.item.padding_bottom,
149
+ )
150
+ .corner_radius(theme.context_menu.item.corner_radius)
151
+ .bg_color(theme.context_menu.item.background);
152
+ if let Some(label_node) = self.label_node.as_ref() {
153
+ label_node
154
+ .font_family(theme.context_menu.item.font_family.clone())
155
+ .font_size(theme.context_menu.item.font_size)
156
+ .text_color(theme.context_menu.item.text_color);
157
+ }
158
+ for dot in &self.dot_nodes {
159
+ dot.bg_color(theme.context_menu.item.text_color);
160
+ }
161
+ }
162
+ }
163
+
164
+ enum ToolbarButtonChild {
165
+ Label(TextNode),
166
+ Overflow((FlexBox, Vec<FlexBox>)),
167
+ }
168
+
169
+ fn create_label(label: &str, slot: i32) -> TextNode {
170
+ let theme = current_theme();
171
+ let label_node = text(label);
172
+ label_node
173
+ .font_family(theme.context_menu.item.font_family.clone())
174
+ .font_size(theme.context_menu.item.font_size)
175
+ .text_color(theme.context_menu.item.text_color)
176
+ .text_overflow(TextOverflow::Ellipsis)
177
+ .selectable(false, theme.colors.selection)
178
+ .interactive(true)
179
+ .preserve_selection_on_pointer_down(true)
180
+ .on_pointer_up(move |event| {
181
+ activate_toolbar_slot(slot);
182
+ event.handled = true;
183
+ });
184
+ label_node
185
+ }
186
+
187
+ fn create_overflow_icon() -> (FlexBox, Vec<FlexBox>) {
188
+ let theme = current_theme();
189
+ let mut dots = Vec::new();
190
+ let icon = flex_box();
191
+ icon.width(16.0, Unit::Pixel)
192
+ .height(theme.context_menu.item.height, Unit::Pixel)
193
+ .flex_direction(FlexDirection::Column)
194
+ .align_items(AlignItems::Center)
195
+ .justify_content(JustifyContent::Center)
196
+ .preserve_selection_on_pointer_down(true)
197
+ .on_pointer_up(|event| {
198
+ activate_toolbar_slot(OVERFLOW_SLOT);
199
+ event.handled = true;
200
+ });
201
+ for _ in 0..3 {
202
+ let dot = flex_box();
203
+ dot.width(3.0, Unit::Pixel)
204
+ .height(3.0, Unit::Pixel)
205
+ .margin(0.0, 1.25, 0.0, 1.25)
206
+ .corner_radius(2.0)
207
+ .bg_color(theme.context_menu.item.text_color);
208
+ icon.child(&dot);
209
+ dots.push(dot);
210
+ }
211
+ (icon, dots)
212
+ }
213
+
214
+ fn make_separator(vertical: bool) -> FlexBox {
215
+ let theme = current_theme();
216
+ let separator = flex_box();
217
+ if vertical {
218
+ separator
219
+ .width(100.0, Unit::Percent)
220
+ .height(1.0, Unit::Pixel)
221
+ .bg_color(theme.context_menu.separator_color);
222
+ } else {
223
+ separator
224
+ .width(1.0, Unit::Pixel)
225
+ .height(theme.context_menu.item.height - 10.0, Unit::Pixel)
226
+ .bg_color(theme.context_menu.separator_color);
227
+ }
228
+ separator
229
+ }
230
+
231
+ pub(crate) fn create_default_host() -> FlexBox {
232
+ STATE.with(|slot| {
233
+ let mut state = slot.borrow_mut();
234
+ if let Some(host_root) = state.host_root.as_ref() {
235
+ return host_root.clone();
236
+ }
237
+ let theme = current_theme();
238
+ let panel = flex_box();
239
+ panel
240
+ .position_type(PositionType::Absolute)
241
+ .height(theme.context_menu.item.height + 8.0, Unit::Pixel)
242
+ .flex_direction(FlexDirection::Row)
243
+ .align_items(AlignItems::Center)
244
+ .padding(
245
+ TOOLBAR_HORIZONTAL_PADDING,
246
+ 4.0,
247
+ TOOLBAR_HORIZONTAL_PADDING,
248
+ 4.0,
249
+ )
250
+ .corner_radius(theme.context_menu.panel_corner_radius)
251
+ .border(1.0, theme.context_menu.panel_border_color)
252
+ .bg_color(theme.context_menu.panel_background)
253
+ .background_blur(10.0)
254
+ .drop_shadow(
255
+ theme.context_menu.panel_shadow_color,
256
+ 0.0,
257
+ theme.context_menu.shadow_offset_y,
258
+ theme.context_menu.shadow_blur,
259
+ theme.context_menu.shadow_spread,
260
+ )
261
+ .preserve_selection_on_pointer_down(true)
262
+ .visibility(Visibility::Collapsed);
263
+
264
+ let overflow_content = flex_box();
265
+ overflow_content
266
+ .flex_direction(FlexDirection::Column)
267
+ .width(100.0, Unit::Percent)
268
+ .bg_color(0x00000000)
269
+ .preserve_selection_on_pointer_down(true);
270
+ let overflow_scroll_box = ScrollBox::new();
271
+ overflow_scroll_box
272
+ .scroll_enabled_x(false)
273
+ .scroll_enabled_y(true)
274
+ .vertical_scrollbar_visibility(crate::node::ScrollBarVisibility::Auto)
275
+ .horizontal_scrollbar_visibility(crate::node::ScrollBarVisibility::Never)
276
+ .scrollbar_gutter(2.0)
277
+ .width(100.0, Unit::Percent)
278
+ .height(VERTICAL_MENU_MAX_HEIGHT, Unit::Pixel)
279
+ .child(&overflow_content)
280
+ .preserve_selection_on_pointer_down(true);
281
+ let overflow_panel = flex_box();
282
+ overflow_panel
283
+ .position_type(PositionType::Absolute)
284
+ .width(VERTICAL_MENU_WIDTH, Unit::Pixel)
285
+ .height(VERTICAL_MENU_MAX_HEIGHT, Unit::Pixel)
286
+ .corner_radius(theme.context_menu.panel_corner_radius)
287
+ .border(1.0, theme.context_menu.panel_border_color)
288
+ .bg_color(theme.context_menu.panel_background)
289
+ .background_blur(10.0)
290
+ .drop_shadow(
291
+ theme.context_menu.panel_shadow_color,
292
+ 0.0,
293
+ theme.context_menu.shadow_offset_y,
294
+ theme.context_menu.shadow_blur,
295
+ theme.context_menu.shadow_spread,
296
+ )
297
+ .preserve_selection_on_pointer_down(true)
298
+ .child(&overflow_scroll_box)
299
+ .visibility(Visibility::Collapsed);
300
+
301
+ state.buttons = vec![
302
+ ToolbarButton::new("Copy", READONLY_BUTTON_WIDTH, 0),
303
+ ToolbarButton::new("Select all", READONLY_BUTTON_WIDTH, 1),
304
+ ToolbarButton::new("Paste", EDITABLE_BUTTON_WIDTH, 2),
305
+ ToolbarButton::new(OVERFLOW_LABEL, OVERFLOW_BUTTON_WIDTH, OVERFLOW_SLOT),
306
+ ];
307
+ state.overflow_buttons = vec![
308
+ ToolbarButton::new("Select all", VERTICAL_MENU_WIDTH, 3),
309
+ ToolbarButton::new("Extra", VERTICAL_MENU_WIDTH, 4),
310
+ ToolbarButton::new("Extra", VERTICAL_MENU_WIDTH, 5),
311
+ ToolbarButton::new("<", VERTICAL_MENU_WIDTH, BACK_SLOT),
312
+ ];
313
+ state.separators = vec![
314
+ make_separator(false),
315
+ make_separator(false),
316
+ make_separator(false),
317
+ ];
318
+ state.overflow_separators = vec![
319
+ make_separator(true),
320
+ make_separator(true),
321
+ make_separator(true),
322
+ make_separator(true),
323
+ ];
324
+
325
+ let host_root = portal();
326
+ host_root
327
+ .position_type(PositionType::Absolute)
328
+ .position(0.0, 0.0)
329
+ .width(100.0, Unit::Percent)
330
+ .height(100.0, Unit::Percent)
331
+ .child(&panel)
332
+ .child(&overflow_panel);
333
+ state.host_root = Some(host_root.clone());
334
+ state.panel = Some(panel);
335
+ state.overflow_panel = Some(overflow_panel);
336
+ state.overflow_scroll_box = Some(overflow_scroll_box);
337
+ state.overflow_content = Some(overflow_content);
338
+ host_root
339
+ })
340
+ }
341
+
342
+ pub(crate) fn reset() {
343
+ STATE.with(|slot| {
344
+ let mut state = slot.borrow_mut();
345
+ if let Some(host_root) = state.host_root.take() {
346
+ host_root.dispose();
347
+ }
348
+ *state = State::new();
349
+ });
350
+ }
351
+
352
+ pub(crate) fn clear() {
353
+ STATE.with(|slot| {
354
+ let mut state = slot.borrow_mut();
355
+ state.active_handle = NodeHandle::INVALID;
356
+ state.active_start = 0;
357
+ state.active_end = 0;
358
+ state.active_cross_selection_text.clear();
359
+ state.active_cross_selection_select_all_target = NodeHandle::INVALID;
360
+ state.active_items.clear();
361
+ state.pending_cross_selection_text_handle = NodeHandle::INVALID;
362
+ state.hidden_for_handle_drag = false;
363
+ state.overflow_visible = false;
364
+ state.horizontal_item_count = 0;
365
+ });
366
+ hide();
367
+ }
368
+
369
+ pub(crate) fn set_pending_cross_selection_text_handle(handle: NodeHandle) {
370
+ STATE.with(|slot| slot.borrow_mut().pending_cross_selection_text_handle = handle);
371
+ }
372
+
373
+ pub(crate) fn handle_selection_changed(
374
+ handle: NodeHandle,
375
+ target: &NodeRef,
376
+ start: u32,
377
+ end: u32,
378
+ selection_chrome_visible: bool,
379
+ ) {
380
+ STATE.with(|slot| slot.borrow_mut().active_cross_selection_text.clear());
381
+ let hidden_for_drag = STATE.with(|slot| slot.borrow().hidden_for_handle_drag);
382
+ let active_handle = STATE.with(|slot| slot.borrow().active_handle);
383
+ if start == end && hidden_for_drag && active_handle == handle {
384
+ STATE.with(|slot| {
385
+ let mut state = slot.borrow_mut();
386
+ state.active_start = start;
387
+ state.active_end = end;
388
+ });
389
+ hide();
390
+ return;
391
+ }
392
+ if !selection_chrome_visible
393
+ || start == end
394
+ || !(target.is_selectable_text_for_routing() || target.is_editable_text_for_routing())
395
+ {
396
+ clear();
397
+ return;
398
+ }
399
+ let text_content = target.text_content_for_routing().unwrap_or_default();
400
+ let selected_text = resolve_selected_text(&text_content, start, end);
401
+ let active_handle = handle;
402
+ STATE.with(|slot| {
403
+ let mut state = slot.borrow_mut();
404
+ state.active_handle = active_handle;
405
+ state.active_start = start;
406
+ state.active_end = end;
407
+ state.active_cross_selection_select_all_target = active_handle;
408
+ });
409
+ create_default_host();
410
+ build_items_for_text(
411
+ active_handle,
412
+ target.is_editable_text_for_routing(),
413
+ &text_content,
414
+ start,
415
+ end,
416
+ &selected_text,
417
+ );
418
+ position_for_text_range(active_handle, start, end);
419
+ }
420
+
421
+ pub(crate) fn handle_cross_selection_changed(
422
+ handle: NodeHandle,
423
+ _area: &NodeRef,
424
+ text: &str,
425
+ selection_chrome_visible: bool,
426
+ ) {
427
+ let hidden_for_drag = STATE.with(|slot| slot.borrow().hidden_for_handle_drag);
428
+ let active_handle = STATE.with(|slot| slot.borrow().active_handle);
429
+ if !selection_chrome_visible || text.is_empty() {
430
+ if text.is_empty() && hidden_for_drag && active_handle == handle {
431
+ STATE.with(|slot| {
432
+ let mut state = slot.borrow_mut();
433
+ state.active_start = 0;
434
+ state.active_end = 0;
435
+ state.active_cross_selection_text.clear();
436
+ });
437
+ hide();
438
+ return;
439
+ }
440
+ clear();
441
+ return;
442
+ }
443
+ let (previous_handle, previous_text, previous_target, pending_handle) = STATE.with(|slot| {
444
+ let state = slot.borrow();
445
+ (
446
+ state.active_handle,
447
+ state.active_cross_selection_text.clone(),
448
+ state.active_cross_selection_select_all_target,
449
+ state.pending_cross_selection_text_handle,
450
+ )
451
+ });
452
+ let mut select_all_target = pending_handle;
453
+ if select_all_target == NodeHandle::INVALID
454
+ && previous_handle == handle
455
+ && previous_text == text
456
+ {
457
+ select_all_target = previous_target;
458
+ }
459
+ if select_all_target == NodeHandle::INVALID {
460
+ select_all_target = handle;
461
+ }
462
+ STATE.with(|slot| {
463
+ let mut state = slot.borrow_mut();
464
+ state.active_handle = handle;
465
+ state.active_start = 0;
466
+ state.active_end = text.chars().count() as u32;
467
+ state.active_cross_selection_text = text.to_owned();
468
+ state.active_cross_selection_select_all_target = select_all_target;
469
+ state.pending_cross_selection_text_handle = NodeHandle::INVALID;
470
+ state.active_items.clear();
471
+ state.active_items.push(
472
+ MenuItem::new("Copy", ContextMenuAction::CopyCurrentSelection).payload(text.to_owned()),
473
+ );
474
+ state.active_items.push(
475
+ MenuItem::new("Select all", ContextMenuAction::SelectAllText)
476
+ .target_handle(select_all_target.raw()),
477
+ );
478
+ });
479
+ apply_items(false);
480
+ position_for_cross_selection(handle);
481
+ }
482
+
483
+ pub(crate) fn refresh_active_geometry(selection_chrome_visible: bool) {
484
+ let (active_handle, hidden_for_handle_drag, has_cross_text, active_start, active_end) = STATE
485
+ .with(|slot| {
486
+ let state = slot.borrow();
487
+ (
488
+ state.active_handle,
489
+ state.hidden_for_handle_drag,
490
+ !state.active_cross_selection_text.is_empty(),
491
+ state.active_start,
492
+ state.active_end,
493
+ )
494
+ });
495
+ if !selection_chrome_visible || active_handle == NodeHandle::INVALID || hidden_for_handle_drag {
496
+ hide();
497
+ return;
498
+ }
499
+ if has_cross_text {
500
+ position_for_cross_selection(active_handle);
501
+ } else {
502
+ position_for_text_range(active_handle, active_start, active_end);
503
+ }
504
+ }
505
+
506
+ pub(crate) fn hide_for_handle_drag() {
507
+ STATE.with(|slot| {
508
+ let mut state = slot.borrow_mut();
509
+ state.hidden_for_handle_drag = true;
510
+ state.overflow_visible = false;
511
+ });
512
+ hide();
513
+ }
514
+
515
+ pub(crate) fn show_after_handle_drag(selection_chrome_visible: bool) {
516
+ STATE.with(|slot| slot.borrow_mut().hidden_for_handle_drag = false);
517
+ refresh_active_geometry(selection_chrome_visible);
518
+ }
519
+
520
+ pub(crate) fn dismiss_for_outside_pointer_down(scene_x: f32, scene_y: f32) -> bool {
521
+ if STATE.with(|slot| slot.borrow().active_items.is_empty()) {
522
+ return false;
523
+ }
524
+ let (panel, overflow_panel) = STATE.with(|slot| {
525
+ let state = slot.borrow();
526
+ (state.panel.clone(), state.overflow_panel.clone())
527
+ });
528
+ if panel
529
+ .as_ref()
530
+ .is_some_and(|panel| point_hits_node(panel, scene_x, scene_y))
531
+ || overflow_panel
532
+ .as_ref()
533
+ .is_some_and(|panel| point_hits_node(panel, scene_x, scene_y))
534
+ || ui::is_point_in_selection(scene_x, scene_y)
535
+ {
536
+ return false;
537
+ }
538
+ hide();
539
+ true
540
+ }
541
+
542
+ fn build_items_for_text(
543
+ handle: NodeHandle,
544
+ editable: bool,
545
+ content: &str,
546
+ start: u32,
547
+ end: u32,
548
+ selected_text: &str,
549
+ ) {
550
+ let selected_payload = (!selected_text.is_empty()).then(|| selected_text.to_owned());
551
+ let has_text = !content.is_empty();
552
+ STATE.with(|slot| {
553
+ let mut state = slot.borrow_mut();
554
+ state.active_items.clear();
555
+ if editable {
556
+ let mut cut = MenuItem::new("Cut", ContextMenuAction::CutTextSelection)
557
+ .target_handle(handle.raw())
558
+ .focus_target_after_action(true)
559
+ .with_selection_range(start, end);
560
+ if let Some(payload) = selected_payload.clone() {
561
+ cut = cut.payload(payload);
562
+ }
563
+ state.active_items.push(cut);
564
+
565
+ let mut copy = MenuItem::new("Copy", ContextMenuAction::CopyCurrentSelection)
566
+ .target_handle(handle.raw())
567
+ .focus_target_after_action(true);
568
+ if let Some(payload) = selected_payload.clone() {
569
+ copy = copy.payload(payload);
570
+ }
571
+ state.active_items.push(copy);
572
+
573
+ state.active_items.push(
574
+ MenuItem::new("Paste", ContextMenuAction::PasteText)
575
+ .target_handle(handle.raw())
576
+ .focus_target_after_action(true),
577
+ );
578
+ state.active_items.push(
579
+ MenuItem::new("Select all", ContextMenuAction::SelectAllText)
580
+ .disabled(!has_text)
581
+ .target_handle(handle.raw())
582
+ .focus_target_after_action(true),
583
+ );
584
+ } else {
585
+ let mut copy = MenuItem::new("Copy", ContextMenuAction::CopyCurrentSelection)
586
+ .target_handle(handle.raw());
587
+ if let Some(payload) = selected_payload {
588
+ copy = copy.payload(payload);
589
+ }
590
+ state.active_items.push(copy);
591
+ state.active_items.push(
592
+ MenuItem::new("Select all", ContextMenuAction::SelectAllText)
593
+ .disabled(!has_text)
594
+ .target_handle(handle.raw()),
595
+ );
596
+ }
597
+ });
598
+ apply_items(editable);
599
+ }
600
+
601
+ fn activate_toolbar_slot(slot: i32) {
602
+ if slot == OVERFLOW_SLOT {
603
+ show_overflow_menu();
604
+ return;
605
+ }
606
+ if slot == BACK_SLOT {
607
+ show_horizontal_menu();
608
+ return;
609
+ }
610
+ let item = STATE.with(|slot_state| {
611
+ let state = slot_state.borrow();
612
+ if slot < 0 || slot as usize >= state.active_items.len() {
613
+ None
614
+ } else {
615
+ Some(state.active_items[slot as usize].clone())
616
+ }
617
+ });
618
+ let Some(item) = item else {
619
+ return;
620
+ };
621
+ run_context_menu_action(&item);
622
+ if item.action == ContextMenuAction::CopyCurrentSelection {
623
+ ui::clear_current_selection();
624
+ clear();
625
+ return;
626
+ }
627
+ if item.action == ContextMenuAction::SelectAllText {
628
+ return;
629
+ }
630
+ hide();
631
+ }
632
+
633
+ fn apply_items(editable: bool) {
634
+ create_default_host();
635
+ let theme = current_theme();
636
+ let button_width = if editable {
637
+ EDITABLE_BUTTON_WIDTH
638
+ } else {
639
+ READONLY_BUTTON_WIDTH
640
+ };
641
+ let (item_count, has_overflow) = STATE.with(|slot| {
642
+ let state = slot.borrow();
643
+ (
644
+ state.active_items.len(),
645
+ state.active_items.len() > MAX_HORIZONTAL_ACTIONS,
646
+ )
647
+ });
648
+ let horizontal_item_count = if has_overflow {
649
+ MAX_HORIZONTAL_ACTIONS + 1
650
+ } else {
651
+ item_count
652
+ };
653
+ STATE.with(|slot| {
654
+ let mut state = slot.borrow_mut();
655
+ state.horizontal_item_count = horizontal_item_count;
656
+ if let Some(panel) = state.panel.as_ref() {
657
+ panel
658
+ .height(theme.context_menu.item.height + 8.0, Unit::Pixel)
659
+ .width(
660
+ active_width_for_button_width(button_width, item_count, has_overflow),
661
+ Unit::Pixel,
662
+ )
663
+ .bg_color(theme.context_menu.panel_background)
664
+ .background_blur(10.0)
665
+ .corner_radius(theme.context_menu.panel_corner_radius)
666
+ .border(1.0, theme.context_menu.panel_border_color)
667
+ .drop_shadow(
668
+ theme.context_menu.panel_shadow_color,
669
+ 0.0,
670
+ theme.context_menu.shadow_offset_y,
671
+ theme.context_menu.shadow_blur,
672
+ theme.context_menu.shadow_spread,
673
+ );
674
+ clear_children(panel);
675
+ let visible_actions = if has_overflow {
676
+ MAX_HORIZONTAL_ACTIONS
677
+ } else {
678
+ item_count
679
+ };
680
+ for index in 0..visible_actions {
681
+ if index > 0 {
682
+ let separator = &state.separators[index - 1];
683
+ separator
684
+ .height(theme.context_menu.item.height - 10.0, Unit::Pixel)
685
+ .bg_color(theme.context_menu.separator_color);
686
+ panel.child(separator);
687
+ }
688
+ let label = state.active_items[index].label.clone();
689
+ let button = &state.buttons[index];
690
+ button.root.width(button_width, Unit::Pixel);
691
+ button.apply_style();
692
+ button.set_label(&label);
693
+ panel.child(&button.root);
694
+ }
695
+ if has_overflow {
696
+ let separator = &state.separators[MAX_HORIZONTAL_ACTIONS - 1];
697
+ separator
698
+ .height(theme.context_menu.item.height - 10.0, Unit::Pixel)
699
+ .bg_color(theme.context_menu.separator_color);
700
+ panel.child(separator);
701
+ let overflow_button = &state.buttons[MAX_HORIZONTAL_ACTIONS];
702
+ overflow_button
703
+ .root
704
+ .width(OVERFLOW_BUTTON_WIDTH, Unit::Pixel);
705
+ overflow_button.apply_style();
706
+ overflow_button.set_label(OVERFLOW_LABEL);
707
+ panel.child(&overflow_button.root);
708
+ }
709
+ }
710
+ });
711
+ apply_overflow_items(button_width);
712
+ show_horizontal_menu();
713
+ }
714
+
715
+ fn apply_overflow_items(_button_width: f32) {
716
+ let theme = current_theme();
717
+ STATE.with(|slot| {
718
+ let state = slot.borrow();
719
+ let overflow_count = state
720
+ .active_items
721
+ .len()
722
+ .saturating_sub(MAX_HORIZONTAL_ACTIONS);
723
+ let total_rows = overflow_count + 1;
724
+ let content_height = (theme.context_menu.item.height * total_rows as f32)
725
+ + (total_rows.saturating_sub(1) as f32);
726
+ let max_panel_height = theme
727
+ .context_menu
728
+ .item
729
+ .height
730
+ .max(VERTICAL_MENU_MAX_HEIGHT.min(ui::get_viewport_height() - (EDGE_MARGIN * 2.0)));
731
+ let panel_height = content_height.min(max_panel_height);
732
+ if let Some(overflow_panel) = state.overflow_panel.as_ref() {
733
+ overflow_panel
734
+ .width(VERTICAL_MENU_WIDTH, Unit::Pixel)
735
+ .height(panel_height, Unit::Pixel)
736
+ .bg_color(theme.context_menu.panel_background)
737
+ .background_blur(10.0)
738
+ .corner_radius(theme.context_menu.panel_corner_radius)
739
+ .border(1.0, theme.context_menu.panel_border_color)
740
+ .drop_shadow(
741
+ theme.context_menu.panel_shadow_color,
742
+ 0.0,
743
+ theme.context_menu.shadow_offset_y,
744
+ theme.context_menu.shadow_blur,
745
+ theme.context_menu.shadow_spread,
746
+ );
747
+ }
748
+ if let Some(scroll_box) = state.overflow_scroll_box.as_ref() {
749
+ scroll_box
750
+ .width(100.0, Unit::Percent)
751
+ .height(panel_height, Unit::Pixel)
752
+ .scroll_content_size(-1.0, content_height);
753
+ }
754
+ if let Some(content) = state.overflow_content.as_ref() {
755
+ clear_children(content);
756
+ for index in 0..overflow_count {
757
+ if index > 0 {
758
+ let separator = &state.overflow_separators[index - 1];
759
+ separator
760
+ .width(100.0, Unit::Percent)
761
+ .height(1.0, Unit::Pixel)
762
+ .bg_color(theme.context_menu.separator_color);
763
+ content.child(separator);
764
+ }
765
+ let item_index = MAX_HORIZONTAL_ACTIONS + index;
766
+ let button = &state.overflow_buttons[index];
767
+ button.root.width(VERTICAL_MENU_WIDTH, Unit::Pixel);
768
+ button.apply_style();
769
+ button.set_label(&state.active_items[item_index].label);
770
+ content.child(&button.root);
771
+ }
772
+ if overflow_count > 0 {
773
+ let separator = &state.overflow_separators[overflow_count - 1];
774
+ separator
775
+ .width(100.0, Unit::Percent)
776
+ .height(1.0, Unit::Pixel)
777
+ .bg_color(theme.context_menu.separator_color);
778
+ content.child(separator);
779
+ }
780
+ let back_button = state.overflow_buttons.last().expect("back button");
781
+ back_button.root.width(VERTICAL_MENU_WIDTH, Unit::Pixel);
782
+ back_button.apply_style();
783
+ back_button.set_label("<");
784
+ content.child(&back_button.root);
785
+ content.width(100.0, Unit::Percent);
786
+ }
787
+ });
788
+ }
789
+
790
+ fn show_overflow_menu() {
791
+ let (last_x, last_y, last_width, can_show) = STATE.with(|slot| {
792
+ let mut state = slot.borrow_mut();
793
+ let can_show = state.panel.is_some()
794
+ && state.overflow_panel.is_some()
795
+ && state.active_items.len() > MAX_HORIZONTAL_ACTIONS;
796
+ if can_show {
797
+ state.overflow_visible = true;
798
+ }
799
+ (
800
+ state.last_panel_x,
801
+ state.last_panel_y,
802
+ state.last_panel_width,
803
+ can_show,
804
+ )
805
+ });
806
+ if !can_show {
807
+ return;
808
+ }
809
+ STATE.with(|slot| {
810
+ let state = slot.borrow();
811
+ if let Some(panel) = state.panel.as_ref() {
812
+ panel.visibility(Visibility::Collapsed);
813
+ }
814
+ });
815
+ position_overflow_panel(
816
+ last_x,
817
+ last_y,
818
+ last_width,
819
+ current_theme().context_menu.item.height + 8.0,
820
+ );
821
+ STATE.with(|slot| {
822
+ if let Some(overflow_panel) = slot.borrow().overflow_panel.as_ref() {
823
+ overflow_panel.visibility(Visibility::Normal);
824
+ }
825
+ });
826
+ }
827
+
828
+ fn show_horizontal_menu() {
829
+ STATE.with(|slot| {
830
+ let mut state = slot.borrow_mut();
831
+ state.overflow_visible = false;
832
+ if let Some(overflow_panel) = state.overflow_panel.as_ref() {
833
+ overflow_panel.visibility(Visibility::Collapsed);
834
+ }
835
+ if let Some(panel) = state.panel.as_ref() {
836
+ if !state.active_items.is_empty() && !state.hidden_for_handle_drag {
837
+ panel.visibility(Visibility::Normal);
838
+ }
839
+ }
840
+ });
841
+ }
842
+
843
+ fn normalize_start(start: u32, end: u32) -> u32 {
844
+ start.min(end)
845
+ }
846
+
847
+ fn normalize_end(start: u32, end: u32) -> u32 {
848
+ start.max(end)
849
+ }
850
+
851
+ fn resolve_selected_text(content: &str, start: u32, end: u32) -> String {
852
+ if start == end {
853
+ return String::new();
854
+ }
855
+ content
856
+ .chars()
857
+ .skip(normalize_start(start, end) as usize)
858
+ .take((normalize_end(start, end) - normalize_start(start, end)) as usize)
859
+ .collect()
860
+ }
861
+
862
+ fn position_for_text_range(handle: NodeHandle, start: u32, end: u32) {
863
+ if start == end {
864
+ hide();
865
+ return;
866
+ }
867
+ let rects = ui::get_text_range_rects(
868
+ handle.raw(),
869
+ normalize_start(start, end),
870
+ normalize_end(start, end),
871
+ );
872
+ if rects.is_empty() {
873
+ hide();
874
+ return;
875
+ }
876
+ let first = rects.first().copied().unwrap();
877
+ let last = rects.last().copied().unwrap();
878
+ position_at_selection_bounds(
879
+ first.x,
880
+ first.y,
881
+ first.height,
882
+ last.x + last.width,
883
+ last.y + last.height,
884
+ );
885
+ }
886
+
887
+ fn position_for_cross_selection(handle: NodeHandle) {
888
+ let Some(rects) = ui::get_cross_selection_endpoint_rects(handle.raw()) else {
889
+ hide();
890
+ return;
891
+ };
892
+ position_at_selection_bounds(
893
+ rects.start.x,
894
+ rects.start.y,
895
+ rects.start.height,
896
+ rects.end.x + rects.end.width,
897
+ rects.end.y + rects.end.height,
898
+ );
899
+ }
900
+
901
+ fn position_at_selection_bounds(
902
+ start_x: f32,
903
+ top_y: f32,
904
+ start_height: f32,
905
+ _end_x: f32,
906
+ bottom_y: f32,
907
+ ) {
908
+ if STATE.with(|slot| slot.borrow().hidden_for_handle_drag) {
909
+ return;
910
+ }
911
+ let (active_len, overflow_visible) = STATE.with(|slot| {
912
+ let state = slot.borrow();
913
+ (state.active_items.len(), state.overflow_visible)
914
+ });
915
+ let has_overflow = active_len > MAX_HORIZONTAL_ACTIONS;
916
+ let button_width = if active_len > 2 {
917
+ EDITABLE_BUTTON_WIDTH
918
+ } else {
919
+ READONLY_BUTTON_WIDTH
920
+ };
921
+ let width = active_width_for_button_width(button_width, active_len, has_overflow);
922
+ let height = current_theme().context_menu.item.height + 8.0;
923
+ let viewport_width = ui::get_viewport_width();
924
+ let viewport_height = ui::get_viewport_height();
925
+ let max_x = (viewport_width - width - EDGE_MARGIN).max(EDGE_MARGIN);
926
+ let x = (start_x - (width * 0.5)).clamp(EDGE_MARGIN, max_x);
927
+ let top_candidate = top_y - height - TOOLBAR_MARGIN;
928
+ let bottom_candidate = bottom_y + TOOLBAR_MARGIN + 12.0;
929
+ let mut y = top_candidate;
930
+ if top_candidate < EDGE_MARGIN {
931
+ y = bottom_candidate;
932
+ if bottom_candidate + height > viewport_height - EDGE_MARGIN {
933
+ y = top_y + (start_height * 0.5) - (height * 0.5);
934
+ }
935
+ }
936
+ let max_y = (viewport_height - height - EDGE_MARGIN).max(EDGE_MARGIN);
937
+ y = y.clamp(EDGE_MARGIN, max_y);
938
+ STATE.with(|slot| {
939
+ let mut state = slot.borrow_mut();
940
+ state.last_panel_x = x;
941
+ state.last_panel_y = y;
942
+ state.last_panel_width = width;
943
+ if let Some(panel) = state.panel.as_ref() {
944
+ panel.position(x, y);
945
+ if !overflow_visible {
946
+ panel.visibility(Visibility::Normal);
947
+ }
948
+ }
949
+ });
950
+ if overflow_visible {
951
+ position_overflow_panel(x, y, width, height);
952
+ }
953
+ }
954
+
955
+ fn position_overflow_panel(x: f32, y: f32, horizontal_width: f32, horizontal_height: f32) {
956
+ let viewport_width = ui::get_viewport_width();
957
+ let viewport_height = ui::get_viewport_height();
958
+ let panel_height = VERTICAL_MENU_MAX_HEIGHT.min(viewport_height - (EDGE_MARGIN * 2.0));
959
+ let max_x = (viewport_width - VERTICAL_MENU_WIDTH - EDGE_MARGIN).max(EDGE_MARGIN);
960
+ let overflow_x = (x + horizontal_width - VERTICAL_MENU_WIDTH).clamp(EDGE_MARGIN, max_x);
961
+ let mut overflow_y = y + horizontal_height + 4.0;
962
+ if overflow_y + panel_height > viewport_height - EDGE_MARGIN {
963
+ overflow_y = y - panel_height - 4.0;
964
+ }
965
+ let max_y = (viewport_height - panel_height - EDGE_MARGIN).max(EDGE_MARGIN);
966
+ overflow_y = overflow_y.clamp(EDGE_MARGIN, max_y);
967
+ STATE.with(|slot| {
968
+ if let Some(overflow_panel) = slot.borrow().overflow_panel.as_ref() {
969
+ overflow_panel.position(overflow_x, overflow_y);
970
+ }
971
+ });
972
+ }
973
+
974
+ fn point_hits_node(node: &FlexBox, scene_x: f32, scene_y: f32) -> bool {
975
+ if node.handle() == NodeHandle::INVALID || !is_visible_node(node) {
976
+ return false;
977
+ }
978
+ let Some(bounds) = ui::get_bounds(node.handle().raw()) else {
979
+ return false;
980
+ };
981
+ scene_x >= bounds[0]
982
+ && scene_x <= (bounds[0] + bounds[2])
983
+ && scene_y >= bounds[1]
984
+ && scene_y <= (bounds[1] + bounds[3])
985
+ }
986
+
987
+ fn active_width_for_button_width(button_width: f32, item_count: usize, has_overflow: bool) -> f32 {
988
+ let visible_actions = if has_overflow {
989
+ MAX_HORIZONTAL_ACTIONS
990
+ } else {
991
+ item_count
992
+ };
993
+ let visible_count = if has_overflow {
994
+ visible_actions + 1
995
+ } else {
996
+ visible_actions
997
+ };
998
+ let action_width = button_width * visible_actions as f32;
999
+ let overflow_width = if has_overflow {
1000
+ OVERFLOW_BUTTON_WIDTH
1001
+ } else {
1002
+ 0.0
1003
+ };
1004
+ action_width
1005
+ + overflow_width
1006
+ + (visible_count.saturating_sub(1) as f32)
1007
+ + (TOOLBAR_HORIZONTAL_PADDING * 2.0)
1008
+ }
1009
+
1010
+ fn append_child_ref(parent: &FlexBox, child: &NodeRef) {
1011
+ parent.retained_node_ref().append_child_ref(child);
1012
+ }
1013
+
1014
+ fn clear_children(parent: &FlexBox) {
1015
+ for child in parent.retained_node_ref().children() {
1016
+ child.detach_from_parent();
1017
+ }
1018
+ }
1019
+
1020
+ fn is_visible_node(node: &FlexBox) -> bool {
1021
+ node.retained_node_ref().is_visible_for_routing()
1022
+ }
1023
+
1024
+ fn hide() {
1025
+ STATE.with(|slot| {
1026
+ let state = slot.borrow();
1027
+ if let Some(panel) = state.panel.as_ref() {
1028
+ panel.visibility(Visibility::Collapsed);
1029
+ }
1030
+ if let Some(overflow_panel) = state.overflow_panel.as_ref() {
1031
+ overflow_panel.visibility(Visibility::Collapsed);
1032
+ }
1033
+ });
1034
+ }