@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,1367 @@
1
+ use crate::bindings::ui;
2
+ use crate::event::{self, PointerEventArgs, PointerType};
3
+ use crate::ffi::{
4
+ BorderStyle, CursorStyle, HandleValue, KeyEventType, SemanticRole, TextAlign, TextOverflow,
5
+ Unit,
6
+ };
7
+ use crate::logger::warn;
8
+ use crate::navigation;
9
+ use crate::node::{
10
+ column, flex_box, grid, portal, Border, FlexBox, Grid, GridTrack, Node, NodeRef, TextNode,
11
+ WeakFlexBox,
12
+ };
13
+ use crate::popup_presenter::{PopupPresenter, PopupPresenterEventTarget};
14
+ use crate::signal::SubscriptionGuard;
15
+ use crate::theme::{current_theme, subscribe, Theme};
16
+ use crate::FontFamily;
17
+ use std::cell::{Cell, RefCell};
18
+ use std::rc::Rc;
19
+
20
+ const MENU_WIDTH: f32 = 220.0;
21
+ const MENU_SEPARATOR_HEIGHT: f32 = 9.0;
22
+ const MENU_EDGE_PADDING: f32 = 8.0;
23
+ const MAX_ITEMS: usize = 25;
24
+ const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
25
+ const SHORTCUT_SHARED_SIZE_GROUP: &str = "ContextMenuShortcutColumn";
26
+
27
+ thread_local! {
28
+ static ACTIVE_CONTEXT_MENU: RefCell<Option<ContextMenuEventTarget>> = const { RefCell::new(None) };
29
+ }
30
+
31
+ fn is_primary_activation_pointer(event: &PointerEventArgs) -> bool {
32
+ event.button == 0
33
+ || event.pointer_type == PointerType::Touch
34
+ || event.pointer_type == PointerType::Pen
35
+ }
36
+
37
+ fn write_payload_to_clipboard(text: &str) {
38
+ let bytes = text.as_bytes();
39
+ unsafe {
40
+ crate::ffi::fui_copy_text(
41
+ if bytes.is_empty() {
42
+ 0
43
+ } else {
44
+ bytes.as_ptr() as usize
45
+ },
46
+ bytes.len() as u32,
47
+ )
48
+ }
49
+ }
50
+
51
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
52
+ enum MenuItemKind {
53
+ Action,
54
+ Separator,
55
+ }
56
+
57
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
58
+ pub enum ContextMenuAction {
59
+ CopyCurrentSelection = 0,
60
+ ReloadPage = 1,
61
+ OpenLink = 2,
62
+ OpenLinkInNewTab = 3,
63
+ NavigateBack = 4,
64
+ NavigateForward = 5,
65
+ UndoTextEdit = 6,
66
+ RedoTextEdit = 7,
67
+ CutTextSelection = 8,
68
+ PasteText = 9,
69
+ SelectAllText = 10,
70
+ OpenImage = 11,
71
+ OpenImageInNewTab = 12,
72
+ }
73
+
74
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
75
+ pub struct ContextMenuVisibilityChangedEventArgs {
76
+ pub visible: bool,
77
+ }
78
+
79
+ type MenuInvokeCallback = Rc<dyn Fn()>;
80
+ type VisibilityChangedCallback = Rc<dyn Fn(ContextMenuVisibilityChangedEventArgs)>;
81
+
82
+ #[derive(Clone)]
83
+ pub struct MenuItem {
84
+ pub label: String,
85
+ pub action: ContextMenuAction,
86
+ pub payload: Option<String>,
87
+ pub shortcut_label: Option<String>,
88
+ pub disabled: bool,
89
+ target_handle: u64,
90
+ selection_start: u32,
91
+ selection_end: u32,
92
+ focus_target_after_action: bool,
93
+ on_invoke: Option<MenuInvokeCallback>,
94
+ kind: MenuItemKind,
95
+ }
96
+
97
+ impl MenuItem {
98
+ pub fn new(label: impl Into<String>, action: ContextMenuAction) -> Self {
99
+ Self {
100
+ label: label.into(),
101
+ action,
102
+ payload: None,
103
+ shortcut_label: None,
104
+ disabled: false,
105
+ target_handle: HandleValue::Invalid as u64,
106
+ selection_start: 0,
107
+ selection_end: 0,
108
+ focus_target_after_action: false,
109
+ on_invoke: None,
110
+ kind: MenuItemKind::Action,
111
+ }
112
+ }
113
+
114
+ pub fn separator() -> Self {
115
+ Self {
116
+ label: String::new(),
117
+ action: ContextMenuAction::ReloadPage,
118
+ payload: None,
119
+ shortcut_label: None,
120
+ disabled: false,
121
+ target_handle: HandleValue::Invalid as u64,
122
+ selection_start: 0,
123
+ selection_end: 0,
124
+ focus_target_after_action: false,
125
+ on_invoke: None,
126
+ kind: MenuItemKind::Separator,
127
+ }
128
+ }
129
+
130
+ pub fn payload(&self, value: impl Into<String>) -> Self {
131
+ let mut next = self.clone();
132
+ next.payload = Some(value.into());
133
+ next
134
+ }
135
+
136
+ pub fn shortcut_label(&self, value: impl Into<String>) -> Self {
137
+ let mut next = self.clone();
138
+ next.shortcut_label = Some(value.into());
139
+ next
140
+ }
141
+
142
+ pub fn disabled(&self, flag: bool) -> Self {
143
+ let mut next = self.clone();
144
+ next.disabled = flag;
145
+ next
146
+ }
147
+
148
+ pub fn target<T: Node>(&self, target: &T) -> Self {
149
+ let mut next = self.clone();
150
+ next.target_handle = target.handle().raw();
151
+ next
152
+ }
153
+
154
+ pub(crate) fn target_handle(&self, target_handle: u64) -> Self {
155
+ let mut next = self.clone();
156
+ next.target_handle = target_handle;
157
+ next
158
+ }
159
+
160
+ pub fn with_selection_range(&self, start: u32, end: u32) -> Self {
161
+ let mut next = self.clone();
162
+ next.selection_start = start;
163
+ next.selection_end = end;
164
+ next
165
+ }
166
+
167
+ pub fn focus_target_after_action(&self, flag: bool) -> Self {
168
+ let mut next = self.clone();
169
+ next.focus_target_after_action = flag;
170
+ next
171
+ }
172
+
173
+ pub fn on_invoke(&self, callback: impl Fn() + 'static) -> Self {
174
+ let mut next = self.clone();
175
+ next.on_invoke = Some(Rc::new(callback));
176
+ next
177
+ }
178
+
179
+ pub fn is_separator(&self) -> bool {
180
+ self.kind == MenuItemKind::Separator
181
+ }
182
+ }
183
+
184
+ fn commit_focused_text_action_if_needed(item: &MenuItem) {
185
+ if item.focus_target_after_action && item.target_handle != HandleValue::Invalid as u64 {
186
+ unsafe { crate::ffi::fui_commit_text_action_focus(item.target_handle) };
187
+ }
188
+ }
189
+
190
+ pub fn run_context_menu_action(item: &MenuItem) {
191
+ if item.disabled {
192
+ let action_needs_live_selection = item.target_handle != HandleValue::Invalid as u64
193
+ && matches!(
194
+ item.action,
195
+ ContextMenuAction::CopyCurrentSelection | ContextMenuAction::CutTextSelection
196
+ )
197
+ && (unsafe { crate::ffi::fui_has_text_selection_snapshot(item.target_handle) }
198
+ || unsafe { crate::ffi::ui_has_text_selection(item.target_handle) });
199
+ if !action_needs_live_selection {
200
+ return;
201
+ }
202
+ }
203
+
204
+ if let Some(callback) = item.on_invoke.as_ref() {
205
+ callback();
206
+ }
207
+
208
+ match item.action {
209
+ ContextMenuAction::CopyCurrentSelection => {
210
+ if let Some(payload) = item.payload.as_ref() {
211
+ write_payload_to_clipboard(payload);
212
+ commit_focused_text_action_if_needed(item);
213
+ return;
214
+ }
215
+ if item.target_handle != HandleValue::Invalid as u64
216
+ && unsafe { crate::ffi::fui_copy_text_selection_snapshot(item.target_handle) }
217
+ {
218
+ commit_focused_text_action_if_needed(item);
219
+ return;
220
+ }
221
+ if item.target_handle != HandleValue::Invalid as u64 {
222
+ unsafe { crate::ffi::ui_copy_text_selection(item.target_handle) };
223
+ commit_focused_text_action_if_needed(item);
224
+ return;
225
+ }
226
+ unsafe { crate::ffi::ui_copy_current_selection() };
227
+ }
228
+ ContextMenuAction::UndoTextEdit if item.target_handle != HandleValue::Invalid as u64 => {
229
+ unsafe { crate::ffi::ui_undo_text_edit(item.target_handle) };
230
+ commit_focused_text_action_if_needed(item);
231
+ }
232
+ ContextMenuAction::RedoTextEdit if item.target_handle != HandleValue::Invalid as u64 => {
233
+ unsafe { crate::ffi::ui_redo_text_edit(item.target_handle) };
234
+ commit_focused_text_action_if_needed(item);
235
+ }
236
+ ContextMenuAction::CutTextSelection
237
+ if item.target_handle != HandleValue::Invalid as u64 =>
238
+ {
239
+ if let Some(payload) = item.payload.as_ref() {
240
+ write_payload_to_clipboard(payload);
241
+ }
242
+ if item.selection_start != item.selection_end
243
+ && unsafe { crate::ffi::fui_cut_text_selection_snapshot(item.target_handle) }
244
+ {
245
+ commit_focused_text_action_if_needed(item);
246
+ return;
247
+ }
248
+ if unsafe { crate::ffi::fui_cut_text_selection_snapshot(item.target_handle) } {
249
+ commit_focused_text_action_if_needed(item);
250
+ return;
251
+ }
252
+ if item.selection_start != item.selection_end
253
+ && unsafe {
254
+ crate::ffi::fui_delete_focused_text_range(
255
+ item.selection_start,
256
+ item.selection_end,
257
+ )
258
+ }
259
+ {
260
+ commit_focused_text_action_if_needed(item);
261
+ return;
262
+ }
263
+ if unsafe { crate::ffi::fui_cut_focused_text_selection() } {
264
+ commit_focused_text_action_if_needed(item);
265
+ return;
266
+ }
267
+ if item.payload.is_none() {
268
+ unsafe { crate::ffi::fui_copy_text_selection_snapshot(item.target_handle) };
269
+ }
270
+ unsafe { crate::ffi::ui_cut_text_selection(item.target_handle) };
271
+ commit_focused_text_action_if_needed(item);
272
+ }
273
+ ContextMenuAction::PasteText if item.target_handle != HandleValue::Invalid as u64 => {
274
+ unsafe { crate::ffi::ui_paste_text(item.target_handle) };
275
+ commit_focused_text_action_if_needed(item);
276
+ }
277
+ ContextMenuAction::SelectAllText if item.target_handle != HandleValue::Invalid as u64 => {
278
+ unsafe { crate::ffi::ui_select_all_text(item.target_handle) };
279
+ commit_focused_text_action_if_needed(item);
280
+ }
281
+ ContextMenuAction::ReloadPage => unsafe { crate::ffi::fui_reload_page() },
282
+ ContextMenuAction::OpenLink | ContextMenuAction::OpenImage => {
283
+ if let Some(payload) = item.payload.as_ref() {
284
+ navigation::navigate_to(payload, false);
285
+ }
286
+ }
287
+ ContextMenuAction::OpenLinkInNewTab | ContextMenuAction::OpenImageInNewTab => {
288
+ if let Some(payload) = item.payload.as_ref() {
289
+ navigation::navigate_to(payload, true);
290
+ }
291
+ }
292
+ ContextMenuAction::NavigateBack => navigation::navigate_back(),
293
+ ContextMenuAction::NavigateForward => navigation::navigate_forward(),
294
+ _ => {}
295
+ }
296
+ }
297
+
298
+ #[derive(Clone)]
299
+ struct ContextMenuEntryStyle {
300
+ item_height: f32,
301
+ padding_left: f32,
302
+ padding_top: f32,
303
+ padding_right: f32,
304
+ padding_bottom: f32,
305
+ corner_radius: f32,
306
+ text_color: u32,
307
+ background_color: u32,
308
+ hover_background_color: u32,
309
+ font_family: FontFamily,
310
+ font_size: f32,
311
+ }
312
+
313
+ impl ContextMenuEntryStyle {
314
+ fn from_theme(theme: &Theme) -> Self {
315
+ Self {
316
+ item_height: theme.context_menu.item.height,
317
+ padding_left: theme.context_menu.item.padding_left,
318
+ padding_top: theme.context_menu.item.padding_top,
319
+ padding_right: theme.context_menu.item.padding_right,
320
+ padding_bottom: theme.context_menu.item.padding_bottom,
321
+ corner_radius: theme.context_menu.item.corner_radius,
322
+ text_color: theme.context_menu.item.text_color,
323
+ background_color: theme.context_menu.item.background,
324
+ hover_background_color: theme.context_menu.item.hover_background,
325
+ font_family: theme.context_menu.item.font_family.clone(),
326
+ font_size: theme.context_menu.item.font_size,
327
+ }
328
+ }
329
+ }
330
+
331
+ fn context_menu_entry_line_height(style: &ContextMenuEntryStyle) -> f32 {
332
+ let content_height = style.item_height - style.padding_top - style.padding_bottom;
333
+ if content_height > 1.0 {
334
+ content_height
335
+ } else {
336
+ 1.0
337
+ }
338
+ }
339
+
340
+ #[derive(Clone)]
341
+ struct ContextMenuEntry {
342
+ root: Grid,
343
+ label_node: TextNode,
344
+ shortcut_node: TextNode,
345
+ slot: usize,
346
+ hovered: Rc<Cell<bool>>,
347
+ pressed: Rc<Cell<bool>>,
348
+ disabled: Rc<Cell<bool>>,
349
+ style: Rc<RefCell<ContextMenuEntryStyle>>,
350
+ }
351
+
352
+ impl ContextMenuEntry {
353
+ fn new(slot: usize) -> Self {
354
+ let theme = current_theme();
355
+ let root = grid();
356
+ let label_node = TextNode::new("");
357
+ label_node
358
+ .font_family(theme.context_menu.item.font_family.clone())
359
+ .font_size(theme.context_menu.item.font_size)
360
+ .line_height(
361
+ theme.context_menu.item.height
362
+ - theme.context_menu.item.padding_top
363
+ - theme.context_menu.item.padding_bottom,
364
+ )
365
+ .text_color(theme.context_menu.item.text_color)
366
+ .text_overflow(TextOverflow::Ellipsis)
367
+ .selectable(false, theme.colors.selection);
368
+ let shortcut_node = TextNode::new("");
369
+ shortcut_node
370
+ .font_family(theme.context_menu.item.font_family.clone())
371
+ .font_size(theme.context_menu.item.font_size)
372
+ .line_height(
373
+ theme.context_menu.item.height
374
+ - theme.context_menu.item.padding_top
375
+ - theme.context_menu.item.padding_bottom,
376
+ )
377
+ .text_color(theme.colors.text_muted)
378
+ .text_align(TextAlign::Left)
379
+ .selectable(false, theme.colors.selection);
380
+
381
+ root.width(100.0, Unit::Percent)
382
+ .height(theme.context_menu.item.height, Unit::Pixel)
383
+ .padding(
384
+ theme.context_menu.item.padding_left,
385
+ theme.context_menu.item.padding_top,
386
+ theme.context_menu.item.padding_right,
387
+ theme.context_menu.item.padding_bottom,
388
+ )
389
+ .interactive(true)
390
+ .semantic_role(SemanticRole::Button)
391
+ .cursor(CursorStyle::Pointer)
392
+ .columns(vec![GridTrack::star(1.0), GridTrack::auto()])
393
+ .rows(vec![GridTrack::star(1.0)])
394
+ .column_shared_size_group(1, SHORTCUT_SHARED_SIZE_GROUP)
395
+ .place_child(&label_node, 0, 0, 1, 1)
396
+ .place_child(&shortcut_node, 0, 1, 1, 1);
397
+
398
+ let entry = Self {
399
+ root,
400
+ label_node,
401
+ shortcut_node,
402
+ slot,
403
+ hovered: Rc::new(Cell::new(false)),
404
+ pressed: Rc::new(Cell::new(false)),
405
+ disabled: Rc::new(Cell::new(false)),
406
+ style: Rc::new(RefCell::new(ContextMenuEntryStyle::from_theme(&theme))),
407
+ };
408
+ entry.bind_events();
409
+ entry.apply_theme();
410
+ entry
411
+ }
412
+
413
+ fn bind_events(&self) {
414
+ let event_target = self.event_target();
415
+ self.root.on_pointer_enter(move |_event| {
416
+ if event_target.disabled.get() {
417
+ return;
418
+ }
419
+ event_target.hovered.set(true);
420
+ event_target.apply_theme();
421
+ });
422
+
423
+ let event_target = self.event_target();
424
+ self.root.on_pointer_leave(move |_event| {
425
+ event_target.hovered.set(false);
426
+ event_target.pressed.set(false);
427
+ event_target.apply_theme();
428
+ });
429
+
430
+ let event_target = self.event_target();
431
+ self.root.on_pointer_down(move |_event| {
432
+ event_target.pressed.set(!event_target.disabled.get());
433
+ });
434
+
435
+ let event_target = self.event_target();
436
+ self.root.on_pointer_up(move |event| {
437
+ let should_invoke = event_target.pressed.get()
438
+ && event_target.hovered.get()
439
+ && !event_target.disabled.get();
440
+ event_target.pressed.set(false);
441
+ event.handled = true;
442
+ if should_invoke {
443
+ ContextMenu::invoke_active_slot(event_target.slot as i32);
444
+ }
445
+ });
446
+
447
+ let event_target = self.event_target();
448
+ self.root.on_pointer_cancel(move |_event| {
449
+ event_target.pressed.set(false);
450
+ });
451
+ }
452
+
453
+ fn item(&self, item: &MenuItem) {
454
+ self.hovered.set(false);
455
+ self.pressed.set(false);
456
+ self.disabled.set(item.disabled);
457
+ self.root
458
+ .semantic_label(item.label.clone())
459
+ .semantic_disabled(item.disabled)
460
+ .cursor(if item.disabled {
461
+ CursorStyle::Default
462
+ } else {
463
+ CursorStyle::Pointer
464
+ });
465
+ self.label_node.text(item.label.clone());
466
+ self.shortcut_node
467
+ .text(item.shortcut_label.clone().unwrap_or_default());
468
+ self.apply_theme();
469
+ }
470
+
471
+ fn configure_style(&self, style: &ContextMenuEntryStyle) {
472
+ *self.style.borrow_mut() = style.clone();
473
+ self.apply_theme();
474
+ }
475
+
476
+ fn apply_theme(&self) {
477
+ let style = self.style.borrow().clone();
478
+ let theme = current_theme();
479
+ self.root
480
+ .height(style.item_height, Unit::Pixel)
481
+ .padding(
482
+ style.padding_left,
483
+ style.padding_top,
484
+ style.padding_right,
485
+ style.padding_bottom,
486
+ )
487
+ .corner_radius(style.corner_radius)
488
+ .bg_color(if self.hovered.get() && !self.disabled.get() {
489
+ style.hover_background_color
490
+ } else {
491
+ style.background_color
492
+ });
493
+ self.label_node
494
+ .font_family(style.font_family.clone())
495
+ .font_size(style.font_size)
496
+ .line_height(context_menu_entry_line_height(&style))
497
+ .text_color(if self.disabled.get() {
498
+ theme.colors.text_muted
499
+ } else {
500
+ style.text_color
501
+ });
502
+ self.shortcut_node
503
+ .font_family(style.font_family.clone())
504
+ .font_size(style.font_size)
505
+ .line_height(context_menu_entry_line_height(&style))
506
+ .text_color(theme.colors.text_muted);
507
+ }
508
+
509
+ fn event_target(&self) -> ContextMenuEntryEventTarget {
510
+ ContextMenuEntryEventTarget {
511
+ slot: self.slot,
512
+ hovered: self.hovered.clone(),
513
+ pressed: self.pressed.clone(),
514
+ disabled: self.disabled.clone(),
515
+ root: self.root.downgrade(),
516
+ label_node: self.label_node.clone(),
517
+ shortcut_node: self.shortcut_node.clone(),
518
+ style: self.style.clone(),
519
+ }
520
+ }
521
+ }
522
+
523
+ #[derive(Clone)]
524
+ struct ContextMenuEntryEventTarget {
525
+ slot: usize,
526
+ hovered: Rc<Cell<bool>>,
527
+ pressed: Rc<Cell<bool>>,
528
+ disabled: Rc<Cell<bool>>,
529
+ root: WeakFlexBox,
530
+ label_node: TextNode,
531
+ shortcut_node: TextNode,
532
+ style: Rc<RefCell<ContextMenuEntryStyle>>,
533
+ }
534
+
535
+ impl ContextMenuEntryEventTarget {
536
+ fn apply_theme(&self) {
537
+ let style = self.style.borrow().clone();
538
+ let theme = current_theme();
539
+ if let Some(root) = self.root.upgrade() {
540
+ root.bg_color(if self.hovered.get() && !self.disabled.get() {
541
+ style.hover_background_color
542
+ } else {
543
+ style.background_color
544
+ });
545
+ }
546
+ self.label_node
547
+ .font_family(style.font_family.clone())
548
+ .font_size(style.font_size)
549
+ .line_height(context_menu_entry_line_height(&style))
550
+ .text_color(if self.disabled.get() {
551
+ theme.colors.text_muted
552
+ } else {
553
+ style.text_color
554
+ });
555
+ self.shortcut_node
556
+ .font_family(style.font_family.clone())
557
+ .font_size(style.font_size)
558
+ .line_height(context_menu_entry_line_height(&style))
559
+ .text_color(theme.colors.text_muted);
560
+ }
561
+ }
562
+
563
+ #[derive(Clone)]
564
+ struct ContextMenuSeparator {
565
+ root: FlexBox,
566
+ line: FlexBox,
567
+ color: Rc<Cell<u32>>,
568
+ }
569
+
570
+ impl ContextMenuSeparator {
571
+ fn new() -> Self {
572
+ let theme = current_theme();
573
+ let root = column();
574
+ let line = flex_box();
575
+ line.width(100.0, Unit::Percent).height(1.0, Unit::Pixel);
576
+ root.width(100.0, Unit::Percent)
577
+ .height(MENU_SEPARATOR_HEIGHT, Unit::Pixel)
578
+ .padding(4.0, 0.0, 4.0, 0.0)
579
+ .child(&line);
580
+ let separator = Self {
581
+ root,
582
+ line,
583
+ color: Rc::new(Cell::new(theme.context_menu.separator_color)),
584
+ };
585
+ separator.apply_theme();
586
+ separator
587
+ }
588
+
589
+ fn configure_style(&self, color: u32) {
590
+ self.color.set(color);
591
+ self.apply_theme();
592
+ }
593
+
594
+ fn apply_theme(&self) {
595
+ self.line.bg_color(self.color.get());
596
+ }
597
+ }
598
+
599
+ struct ContextMenuState {
600
+ visible: bool,
601
+ suppress_next_pointer_up_activation: bool,
602
+ key_filter_token: u32,
603
+ menu_width: f32,
604
+ item_style: ContextMenuEntryStyle,
605
+ panel_background_color: u32,
606
+ panel_border_width: f32,
607
+ panel_border_color: u32,
608
+ panel_border_style: BorderStyle,
609
+ panel_corner_radius: f32,
610
+ separator_color: u32,
611
+ panel_shadow_color: u32,
612
+ panel_shadow_offset_y: f32,
613
+ panel_shadow_blur: f32,
614
+ panel_shadow_spread: f32,
615
+ panel_background_blur_sigma: f32,
616
+ panel_background_overridden: bool,
617
+ panel_border_overridden: bool,
618
+ panel_corner_radius_overridden: bool,
619
+ separator_color_overridden: bool,
620
+ panel_shadow_overridden: bool,
621
+ panel_background_blur_overridden: bool,
622
+ item_metrics_overridden: bool,
623
+ item_text_color_overridden: bool,
624
+ item_background_overridden: bool,
625
+ item_hover_color_overridden: bool,
626
+ item_corner_radius_overridden: bool,
627
+ item_font_overridden: bool,
628
+ visibility_changed_callback: Option<VisibilityChangedCallback>,
629
+ }
630
+
631
+ impl ContextMenuState {
632
+ fn from_theme(theme: Theme) -> Self {
633
+ Self {
634
+ visible: false,
635
+ suppress_next_pointer_up_activation: false,
636
+ key_filter_token: 0,
637
+ menu_width: MENU_WIDTH,
638
+ item_style: ContextMenuEntryStyle::from_theme(&theme),
639
+ panel_background_color: theme.context_menu.panel_background,
640
+ panel_border_width: 1.0,
641
+ panel_border_color: theme.context_menu.panel_border_color,
642
+ panel_border_style: BorderStyle::Solid,
643
+ panel_corner_radius: theme.context_menu.panel_corner_radius,
644
+ separator_color: theme.context_menu.separator_color,
645
+ panel_shadow_color: theme.context_menu.panel_shadow_color,
646
+ panel_shadow_offset_y: theme.context_menu.shadow_offset_y,
647
+ panel_shadow_blur: theme.context_menu.shadow_blur,
648
+ panel_shadow_spread: theme.context_menu.shadow_spread,
649
+ panel_background_blur_sigma: DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA,
650
+ panel_background_overridden: false,
651
+ panel_border_overridden: false,
652
+ panel_corner_radius_overridden: false,
653
+ separator_color_overridden: false,
654
+ panel_shadow_overridden: false,
655
+ panel_background_blur_overridden: false,
656
+ item_metrics_overridden: false,
657
+ item_text_color_overridden: false,
658
+ item_background_overridden: false,
659
+ item_hover_color_overridden: false,
660
+ item_corner_radius_overridden: false,
661
+ item_font_overridden: false,
662
+ visibility_changed_callback: None,
663
+ }
664
+ }
665
+ }
666
+
667
+ #[derive(Clone)]
668
+ struct ContextMenuEventTarget {
669
+ panel: WeakFlexBox,
670
+ presenter: PopupPresenterEventTarget,
671
+ presenter_owner: PopupPresenter,
672
+ entries: Vec<ContextMenuEntry>,
673
+ separators: Vec<ContextMenuSeparator>,
674
+ current_items: Rc<RefCell<Vec<MenuItem>>>,
675
+ current_item_tops: Rc<RefCell<Vec<f32>>>,
676
+ current_item_heights: Rc<RefCell<Vec<f32>>>,
677
+ state: Rc<RefCell<ContextMenuState>>,
678
+ }
679
+
680
+ impl ContextMenuEventTarget {
681
+ fn clear_panel(&self) {
682
+ let Some(panel) = self.panel.upgrade() else {
683
+ return;
684
+ };
685
+ for entry in &self.entries {
686
+ panel.remove_child(&entry.root);
687
+ }
688
+ for separator in &self.separators {
689
+ panel.remove_child(&separator.root);
690
+ }
691
+ }
692
+
693
+ fn hide(&self) {
694
+ let was_visible = self.state.borrow().visible || self.presenter_owner.is_open();
695
+ if !was_visible {
696
+ return;
697
+ }
698
+ self.clear_panel();
699
+ self.current_items.borrow_mut().clear();
700
+ self.current_item_tops.borrow_mut().clear();
701
+ self.current_item_heights.borrow_mut().clear();
702
+ self.presenter.hide();
703
+ let callback = {
704
+ let mut state = self.state.borrow_mut();
705
+ state.visible = false;
706
+ state.suppress_next_pointer_up_activation = false;
707
+ let token = state.key_filter_token;
708
+ state.key_filter_token = 0;
709
+ if token != 0 {
710
+ event::remove_key_filter(token);
711
+ }
712
+ state.visibility_changed_callback.clone()
713
+ };
714
+ ACTIVE_CONTEXT_MENU.with(|slot| {
715
+ let should_clear = slot
716
+ .borrow()
717
+ .as_ref()
718
+ .is_some_and(|active| Rc::ptr_eq(&active.state, &self.state));
719
+ if should_clear {
720
+ slot.borrow_mut().take();
721
+ }
722
+ });
723
+ if let Some(callback) = callback {
724
+ callback(ContextMenuVisibilityChangedEventArgs { visible: false });
725
+ }
726
+ }
727
+
728
+ fn invoke_slot(&self, slot: usize) {
729
+ let item = self.current_items.borrow().get(slot).cloned();
730
+ if let Some(item) = item {
731
+ run_context_menu_action(&item);
732
+ self.hide();
733
+ }
734
+ }
735
+
736
+ fn handle_overlay_pointer_up(&self, event: &mut PointerEventArgs) {
737
+ if !self.state.borrow().visible {
738
+ return;
739
+ }
740
+ if !is_primary_activation_pointer(event) {
741
+ if ContextMenu::consume_opening_pointer_up_suppression() {
742
+ event.handled = true;
743
+ return;
744
+ }
745
+ event.handled = true;
746
+ return;
747
+ }
748
+ let item_count = self.current_items.borrow().len();
749
+ for slot in 0..item_count {
750
+ let entry = &self.entries[slot];
751
+ let Some(bounds) = ui::get_bounds(entry.root.handle().raw()) else {
752
+ continue;
753
+ };
754
+ let left = bounds[0];
755
+ let top = bounds[1];
756
+ let right = left + bounds[2];
757
+ let bottom = top + bounds[3];
758
+ if event.scene_x >= left
759
+ && event.scene_x <= right
760
+ && event.scene_y >= top
761
+ && event.scene_y <= bottom
762
+ {
763
+ event.handled = true;
764
+ self.invoke_slot(slot);
765
+ return;
766
+ }
767
+ }
768
+ let local_x = event.scene_x - self.presenter_owner.surface_x();
769
+ let local_y = event.scene_y - self.presenter_owner.surface_y();
770
+ if local_x < 0.0 || local_x > self.state.borrow().menu_width || local_y < 0.0 {
771
+ event.handled = true;
772
+ self.hide();
773
+ return;
774
+ }
775
+ let slot_to_invoke = {
776
+ let tops = self.current_item_tops.borrow();
777
+ let heights = self.current_item_heights.borrow();
778
+ let mut slot_to_invoke = None;
779
+ for slot in 0..tops.len() {
780
+ let top = tops[slot];
781
+ let height = heights[slot];
782
+ if local_y >= top && local_y <= top + height {
783
+ slot_to_invoke = Some(slot);
784
+ break;
785
+ }
786
+ }
787
+ slot_to_invoke
788
+ };
789
+ if let Some(slot) = slot_to_invoke {
790
+ event.handled = true;
791
+ self.invoke_slot(slot);
792
+ return;
793
+ }
794
+ event.handled = true;
795
+ self.hide();
796
+ }
797
+
798
+ fn handle_global_key_event(&self, event_type: KeyEventType, key: &str) -> bool {
799
+ if event_type == KeyEventType::Down && key == "Escape" {
800
+ self.hide();
801
+ return true;
802
+ }
803
+ false
804
+ }
805
+
806
+ fn apply_theme(&self) {
807
+ let Some(panel) = self.panel.upgrade() else {
808
+ return;
809
+ };
810
+ let state = self.state.borrow();
811
+ panel
812
+ .shared_size_scope(true)
813
+ .width(state.menu_width, Unit::Pixel)
814
+ .bg_color(state.panel_background_color)
815
+ .background_blur(state.panel_background_blur_sigma)
816
+ .corner_radius(state.panel_corner_radius)
817
+ .border_config(Border {
818
+ width: state.panel_border_width,
819
+ color: state.panel_border_color,
820
+ style: state.panel_border_style,
821
+ dash_on: 0.0,
822
+ dash_off: 0.0,
823
+ })
824
+ .drop_shadow(
825
+ state.panel_shadow_color,
826
+ 0.0,
827
+ state.panel_shadow_offset_y,
828
+ state.panel_shadow_blur,
829
+ state.panel_shadow_spread,
830
+ );
831
+ let item_style = state.item_style.clone();
832
+ let separator_color = state.separator_color;
833
+ drop(state);
834
+ for entry in &self.entries {
835
+ entry.configure_style(&item_style);
836
+ }
837
+ for separator in &self.separators {
838
+ separator.configure_style(separator_color);
839
+ }
840
+ }
841
+
842
+ fn handle_theme_changed(&self) {
843
+ let theme = current_theme();
844
+ let mut state = self.state.borrow_mut();
845
+ if !state.panel_background_overridden {
846
+ state.panel_background_color = theme.context_menu.panel_background;
847
+ }
848
+ if !state.panel_border_overridden {
849
+ state.panel_border_width = 1.0;
850
+ state.panel_border_color = theme.context_menu.panel_border_color;
851
+ state.panel_border_style = BorderStyle::Solid;
852
+ }
853
+ if !state.panel_corner_radius_overridden {
854
+ state.panel_corner_radius = theme.context_menu.panel_corner_radius;
855
+ }
856
+ if !state.separator_color_overridden {
857
+ state.separator_color = theme.context_menu.separator_color;
858
+ }
859
+ if !state.panel_shadow_overridden {
860
+ state.panel_shadow_color = theme.context_menu.panel_shadow_color;
861
+ state.panel_shadow_offset_y = theme.context_menu.shadow_offset_y;
862
+ state.panel_shadow_blur = theme.context_menu.shadow_blur;
863
+ state.panel_shadow_spread = theme.context_menu.shadow_spread;
864
+ }
865
+ if !state.panel_background_blur_overridden {
866
+ state.panel_background_blur_sigma = DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA;
867
+ }
868
+ if !state.item_metrics_overridden {
869
+ state.item_style.item_height = theme.context_menu.item.height;
870
+ state.item_style.padding_left = theme.context_menu.item.padding_left;
871
+ state.item_style.padding_top = theme.context_menu.item.padding_top;
872
+ state.item_style.padding_right = theme.context_menu.item.padding_right;
873
+ state.item_style.padding_bottom = theme.context_menu.item.padding_bottom;
874
+ }
875
+ if !state.item_text_color_overridden {
876
+ state.item_style.text_color = theme.context_menu.item.text_color;
877
+ }
878
+ if !state.item_background_overridden {
879
+ state.item_style.background_color = theme.context_menu.item.background;
880
+ }
881
+ if !state.item_hover_color_overridden {
882
+ state.item_style.hover_background_color = theme.context_menu.item.hover_background;
883
+ }
884
+ if !state.item_corner_radius_overridden {
885
+ state.item_style.corner_radius = theme.context_menu.item.corner_radius;
886
+ }
887
+ if !state.item_font_overridden {
888
+ state.item_style.font_family = theme.context_menu.item.font_family.clone();
889
+ state.item_style.font_size = theme.context_menu.item.font_size;
890
+ }
891
+ drop(state);
892
+ self.apply_theme();
893
+ }
894
+ }
895
+
896
+ #[derive(Clone)]
897
+ pub struct ContextMenu {
898
+ root: FlexBox,
899
+ panel: FlexBox,
900
+ popup_presenter: PopupPresenter,
901
+ items: Rc<RefCell<Vec<MenuItem>>>,
902
+ entries: Vec<ContextMenuEntry>,
903
+ separators: Vec<ContextMenuSeparator>,
904
+ current_items: Rc<RefCell<Vec<MenuItem>>>,
905
+ current_item_tops: Rc<RefCell<Vec<f32>>>,
906
+ current_item_heights: Rc<RefCell<Vec<f32>>>,
907
+ state: Rc<RefCell<ContextMenuState>>,
908
+ theme_guard: Rc<RefCell<Option<SubscriptionGuard>>>,
909
+ }
910
+
911
+ impl Default for ContextMenu {
912
+ fn default() -> Self {
913
+ Self::new()
914
+ }
915
+ }
916
+
917
+ impl ContextMenu {
918
+ pub fn new() -> Self {
919
+ let theme = current_theme();
920
+ let root = portal();
921
+ let panel = column();
922
+ panel
923
+ .position_type(crate::ffi::PositionType::Absolute)
924
+ .shared_size_scope(true)
925
+ .width(MENU_WIDTH, Unit::Pixel)
926
+ .padding(4.0, 4.0, 4.0, 4.0)
927
+ .border(1.0, theme.context_menu.panel_border_color);
928
+ let popup_presenter = PopupPresenter::new(root.clone(), panel.clone());
929
+ root.position_type(crate::ffi::PositionType::Absolute)
930
+ .position(0.0, 0.0)
931
+ .width(100.0, Unit::Percent)
932
+ .height(100.0, Unit::Percent);
933
+
934
+ let menu = Self {
935
+ root,
936
+ panel,
937
+ popup_presenter,
938
+ items: Rc::new(RefCell::new(Vec::new())),
939
+ entries: (0..MAX_ITEMS).map(ContextMenuEntry::new).collect(),
940
+ separators: (0..MAX_ITEMS)
941
+ .map(|_| ContextMenuSeparator::new())
942
+ .collect(),
943
+ current_items: Rc::new(RefCell::new(Vec::new())),
944
+ current_item_tops: Rc::new(RefCell::new(Vec::new())),
945
+ current_item_heights: Rc::new(RefCell::new(Vec::new())),
946
+ state: Rc::new(RefCell::new(ContextMenuState::from_theme(theme))),
947
+ theme_guard: Rc::new(RefCell::new(None)),
948
+ };
949
+ menu.bind_events();
950
+ menu.install_theme_subscription();
951
+ menu.apply_theme();
952
+ menu
953
+ }
954
+
955
+ pub fn hide_active_menu() {
956
+ let menu = ACTIVE_CONTEXT_MENU.with(|slot| slot.borrow().as_ref().cloned());
957
+ if let Some(menu) = menu {
958
+ menu.hide();
959
+ }
960
+ }
961
+
962
+ pub(crate) fn invoke_active_slot(slot: i32) {
963
+ if slot < 0 {
964
+ return;
965
+ }
966
+ let menu = ACTIVE_CONTEXT_MENU.with(|slot_state| slot_state.borrow().as_ref().cloned());
967
+ if let Some(menu) = menu {
968
+ menu.invoke_slot(slot as usize);
969
+ }
970
+ }
971
+
972
+ pub(crate) fn consume_opening_pointer_up_suppression() -> bool {
973
+ ACTIVE_CONTEXT_MENU.with(|slot| {
974
+ let Some(menu) = slot.borrow().as_ref().cloned() else {
975
+ return false;
976
+ };
977
+ let mut state = menu.state.borrow_mut();
978
+ if !state.suppress_next_pointer_up_activation {
979
+ return false;
980
+ }
981
+ state.suppress_next_pointer_up_activation = false;
982
+ true
983
+ })
984
+ }
985
+
986
+ pub fn items<I>(&self, items: I) -> &Self
987
+ where
988
+ I: IntoIterator<Item = MenuItem>,
989
+ {
990
+ self.items.borrow_mut().clear();
991
+ self.items.borrow_mut().extend(items);
992
+ self
993
+ }
994
+
995
+ pub fn clear_items(&self) -> &Self {
996
+ self.items.borrow_mut().clear();
997
+ self
998
+ }
999
+
1000
+ pub fn is_open(&self) -> bool {
1001
+ self.state.borrow().visible
1002
+ }
1003
+
1004
+ pub fn on_visibility_changed(
1005
+ &self,
1006
+ handler: impl Fn(ContextMenuVisibilityChangedEventArgs) + 'static,
1007
+ ) -> &Self {
1008
+ self.state.borrow_mut().visibility_changed_callback = Some(Rc::new(handler));
1009
+ self
1010
+ }
1011
+
1012
+ pub fn clear_visibility_changed(&self) -> &Self {
1013
+ self.state.borrow_mut().visibility_changed_callback = None;
1014
+ self
1015
+ }
1016
+
1017
+ pub fn menu_width(&self, value: f32) -> &Self {
1018
+ if value <= 0.0 {
1019
+ warn(
1020
+ "Layout",
1021
+ &format!("ContextMenu.menu_width() received {value}; clamping to 1.0."),
1022
+ );
1023
+ }
1024
+ self.state.borrow_mut().menu_width = value.max(1.0);
1025
+ self.apply_theme();
1026
+ self
1027
+ }
1028
+
1029
+ pub fn item_height(&self, value: f32) -> &Self {
1030
+ if value <= 0.0 {
1031
+ warn(
1032
+ "Layout",
1033
+ &format!("ContextMenu.item_height() received {value}; clamping to 1.0."),
1034
+ );
1035
+ }
1036
+ let mut state = self.state.borrow_mut();
1037
+ state.item_metrics_overridden = true;
1038
+ state.item_style.item_height = value.max(1.0);
1039
+ drop(state);
1040
+ self.apply_theme();
1041
+ self
1042
+ }
1043
+
1044
+ pub fn item_padding(&self, left: f32, top: f32, right: f32, bottom: f32) -> &Self {
1045
+ let mut state = self.state.borrow_mut();
1046
+ state.item_metrics_overridden = true;
1047
+ state.item_style.padding_left = left;
1048
+ state.item_style.padding_top = top;
1049
+ state.item_style.padding_right = right;
1050
+ state.item_style.padding_bottom = bottom;
1051
+ drop(state);
1052
+ self.apply_theme();
1053
+ self
1054
+ }
1055
+
1056
+ pub fn panel_color(&self, color: u32) -> &Self {
1057
+ let mut state = self.state.borrow_mut();
1058
+ state.panel_background_overridden = true;
1059
+ state.panel_background_color = color;
1060
+ drop(state);
1061
+ self.apply_theme();
1062
+ self
1063
+ }
1064
+
1065
+ pub fn panel_border(&self, width: f32, color: u32) -> &Self {
1066
+ let mut state = self.state.borrow_mut();
1067
+ state.panel_border_overridden = true;
1068
+ state.panel_border_width = width;
1069
+ state.panel_border_color = color;
1070
+ state.panel_border_style = BorderStyle::Solid;
1071
+ drop(state);
1072
+ self.apply_theme();
1073
+ self
1074
+ }
1075
+
1076
+ pub fn panel_border_config(&self, border: Border) -> &Self {
1077
+ let mut state = self.state.borrow_mut();
1078
+ state.panel_border_overridden = true;
1079
+ state.panel_border_width = border.width;
1080
+ state.panel_border_color = border.color;
1081
+ state.panel_border_style = border.style;
1082
+ drop(state);
1083
+ self.apply_theme();
1084
+ self
1085
+ }
1086
+
1087
+ pub fn panel_corner_radius(&self, radius: f32) -> &Self {
1088
+ let mut state = self.state.borrow_mut();
1089
+ state.panel_corner_radius_overridden = true;
1090
+ state.panel_corner_radius = radius.max(0.0);
1091
+ drop(state);
1092
+ self.apply_theme();
1093
+ self
1094
+ }
1095
+
1096
+ pub fn panel_shadow(&self, color: u32, offset_y: f32, blur_sigma: f32, spread: f32) -> &Self {
1097
+ let mut state = self.state.borrow_mut();
1098
+ state.panel_shadow_overridden = true;
1099
+ state.panel_shadow_color = color;
1100
+ state.panel_shadow_offset_y = offset_y;
1101
+ state.panel_shadow_blur = blur_sigma;
1102
+ state.panel_shadow_spread = spread;
1103
+ drop(state);
1104
+ self.apply_theme();
1105
+ self
1106
+ }
1107
+
1108
+ pub fn panel_background_blur(&self, sigma: f32) -> &Self {
1109
+ if sigma < 0.0 {
1110
+ warn(
1111
+ "Layout",
1112
+ &format!("ContextMenu.panel_background_blur() received {sigma}; clamping to 0.0."),
1113
+ );
1114
+ }
1115
+ let mut state = self.state.borrow_mut();
1116
+ state.panel_background_blur_overridden = true;
1117
+ state.panel_background_blur_sigma = sigma.max(0.0);
1118
+ drop(state);
1119
+ self.apply_theme();
1120
+ self
1121
+ }
1122
+
1123
+ pub fn backdrop_color(&self, color: u32) -> &Self {
1124
+ self.popup_presenter.backdrop_color(color);
1125
+ self
1126
+ }
1127
+
1128
+ pub fn background_blur(&self, sigma: f32) -> &Self {
1129
+ self.popup_presenter.background_blur(sigma);
1130
+ self
1131
+ }
1132
+
1133
+ pub fn item_color(&self, color: u32) -> &Self {
1134
+ let mut state = self.state.borrow_mut();
1135
+ state.item_background_overridden = true;
1136
+ state.item_style.background_color = color;
1137
+ drop(state);
1138
+ self.apply_theme();
1139
+ self
1140
+ }
1141
+
1142
+ pub fn item_hover_color(&self, color: u32) -> &Self {
1143
+ let mut state = self.state.borrow_mut();
1144
+ state.item_hover_color_overridden = true;
1145
+ state.item_style.hover_background_color = color;
1146
+ drop(state);
1147
+ self.apply_theme();
1148
+ self
1149
+ }
1150
+
1151
+ pub fn item_text_color(&self, color: u32) -> &Self {
1152
+ let mut state = self.state.borrow_mut();
1153
+ state.item_text_color_overridden = true;
1154
+ state.item_style.text_color = color;
1155
+ drop(state);
1156
+ self.apply_theme();
1157
+ self
1158
+ }
1159
+
1160
+ pub fn item_corner_radius(&self, radius: f32) -> &Self {
1161
+ let mut state = self.state.borrow_mut();
1162
+ state.item_corner_radius_overridden = true;
1163
+ state.item_style.corner_radius = radius.max(0.0);
1164
+ drop(state);
1165
+ self.apply_theme();
1166
+ self
1167
+ }
1168
+
1169
+ pub fn item_font(&self, font_family: FontFamily, font_size: f32) -> &Self {
1170
+ let mut state = self.state.borrow_mut();
1171
+ state.item_font_overridden = true;
1172
+ state.item_style.font_family = font_family;
1173
+ state.item_style.font_size = font_size;
1174
+ drop(state);
1175
+ self.apply_theme();
1176
+ self
1177
+ }
1178
+
1179
+ pub fn separator_color(&self, color: u32) -> &Self {
1180
+ let mut state = self.state.borrow_mut();
1181
+ state.separator_color_overridden = true;
1182
+ state.separator_color = color;
1183
+ drop(state);
1184
+ self.apply_theme();
1185
+ self
1186
+ }
1187
+
1188
+ pub fn show(&self, x: f32, y: f32) {
1189
+ self.show_impl(x, y, false);
1190
+ }
1191
+
1192
+ pub fn show_relative_to<T: Node>(&self, target: &T, x: f32, y: f32) {
1193
+ if let Some(bounds) = ui::get_bounds(target.handle().raw()) {
1194
+ self.show(bounds[0] + x, bounds[1] + y);
1195
+ } else {
1196
+ self.show(x, y);
1197
+ }
1198
+ }
1199
+
1200
+ pub fn show_from_context_pointer(&self, x: f32, y: f32) {
1201
+ self.show_impl(x, y, true);
1202
+ }
1203
+
1204
+ pub fn show_from_context_pointer_relative_to<T: Node>(&self, target: &T, x: f32, y: f32) {
1205
+ if let Some(bounds) = ui::get_bounds(target.handle().raw()) {
1206
+ self.show_from_context_pointer(bounds[0] + x, bounds[1] + y);
1207
+ } else {
1208
+ self.show_from_context_pointer(x, y);
1209
+ }
1210
+ }
1211
+
1212
+ fn show_impl(&self, x: f32, y: f32, suppress_opening_pointer_up: bool) {
1213
+ self.clear_panel();
1214
+ self.apply_theme();
1215
+ self.current_items.borrow_mut().clear();
1216
+ self.current_item_tops.borrow_mut().clear();
1217
+ self.current_item_heights.borrow_mut().clear();
1218
+
1219
+ let items = self.items.borrow().clone();
1220
+ let item_height = self.state.borrow().item_style.item_height;
1221
+ let menu_width = self.state.borrow().menu_width;
1222
+ let mut action_count = 0usize;
1223
+ let mut separator_count = 0usize;
1224
+ let mut estimated_height = 8.0;
1225
+ let mut content_y = 0.0;
1226
+ let mut last_was_separator = true;
1227
+ let count = items.len().min(MAX_ITEMS);
1228
+
1229
+ if items.len() > MAX_ITEMS {
1230
+ warn(
1231
+ "Layout",
1232
+ &format!(
1233
+ "ContextMenu.show() received {} items; truncating to {MAX_ITEMS}.",
1234
+ items.len()
1235
+ ),
1236
+ );
1237
+ }
1238
+
1239
+ for (index, item) in items.into_iter().take(count).enumerate() {
1240
+ if item.is_separator() {
1241
+ if last_was_separator || index == count - 1 {
1242
+ continue;
1243
+ }
1244
+ let separator = &self.separators[separator_count];
1245
+ separator.apply_theme();
1246
+ self.panel.child(&separator.root);
1247
+ separator_count += 1;
1248
+ estimated_height += MENU_SEPARATOR_HEIGHT;
1249
+ content_y += MENU_SEPARATOR_HEIGHT;
1250
+ last_was_separator = true;
1251
+ continue;
1252
+ }
1253
+
1254
+ let entry = &self.entries[action_count];
1255
+ entry.item(&item);
1256
+ entry.apply_theme();
1257
+ self.current_items.borrow_mut().push(item);
1258
+ self.current_item_tops.borrow_mut().push(content_y);
1259
+ self.current_item_heights.borrow_mut().push(item_height);
1260
+ self.panel.child(&entry.root);
1261
+ action_count += 1;
1262
+ estimated_height += item_height;
1263
+ content_y += item_height;
1264
+ last_was_separator = false;
1265
+ }
1266
+
1267
+ if action_count == 0 {
1268
+ self.clear_panel();
1269
+ return;
1270
+ }
1271
+
1272
+ let max_x = (ui::get_viewport_width() - menu_width - MENU_EDGE_PADDING).max(0.0);
1273
+ let max_y = (ui::get_viewport_height() - estimated_height - MENU_EDGE_PADDING).max(0.0);
1274
+ let clamped_x = x.clamp(MENU_EDGE_PADDING, max_x);
1275
+ let clamped_y = y.clamp(MENU_EDGE_PADDING, max_y);
1276
+ self.popup_presenter
1277
+ .show_at_point(clamped_x, clamped_y, menu_width, estimated_height);
1278
+ {
1279
+ let mut state = self.state.borrow_mut();
1280
+ state.visible = true;
1281
+ state.suppress_next_pointer_up_activation = suppress_opening_pointer_up;
1282
+ }
1283
+ ACTIVE_CONTEXT_MENU.with(|slot| {
1284
+ *slot.borrow_mut() = Some(self.event_target());
1285
+ });
1286
+ if let Some(callback) = self.state.borrow().visibility_changed_callback.clone() {
1287
+ callback(ContextMenuVisibilityChangedEventArgs { visible: true });
1288
+ }
1289
+ if self.state.borrow().key_filter_token == 0 {
1290
+ let event_target = self.event_target();
1291
+ let token = event::push_key_filter(move |event_type, key, _modifiers| {
1292
+ event_target.handle_global_key_event(event_type, key)
1293
+ });
1294
+ self.state.borrow_mut().key_filter_token = token;
1295
+ }
1296
+ }
1297
+
1298
+ pub fn hide(&self) {
1299
+ self.event_target().hide();
1300
+ }
1301
+
1302
+ fn bind_events(&self) {
1303
+ let event_target = self.event_target();
1304
+ self.popup_presenter
1305
+ .overlay_node()
1306
+ .interactive(true)
1307
+ .on_pointer_up(move |event| {
1308
+ event_target.handle_overlay_pointer_up(event);
1309
+ });
1310
+ }
1311
+
1312
+ fn install_theme_subscription(&self) {
1313
+ let event_target = self.event_target();
1314
+ *self.theme_guard.borrow_mut() = Some(subscribe(move |_theme| {
1315
+ event_target.handle_theme_changed();
1316
+ }));
1317
+ }
1318
+
1319
+ fn clear_panel(&self) {
1320
+ self.event_target().clear_panel();
1321
+ }
1322
+
1323
+ fn apply_theme(&self) {
1324
+ self.event_target().apply_theme();
1325
+ }
1326
+
1327
+ fn event_target(&self) -> ContextMenuEventTarget {
1328
+ ContextMenuEventTarget {
1329
+ panel: self.panel.downgrade(),
1330
+ presenter: self.popup_presenter.event_target(),
1331
+ presenter_owner: self.popup_presenter.clone(),
1332
+ entries: self.entries.clone(),
1333
+ separators: self.separators.clone(),
1334
+ current_items: self.current_items.clone(),
1335
+ current_item_tops: self.current_item_tops.clone(),
1336
+ current_item_heights: self.current_item_heights.clone(),
1337
+ state: self.state.clone(),
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ impl Node for ContextMenu {
1343
+ fn retained_node_ref(&self) -> NodeRef {
1344
+ self.root.retained_node_ref()
1345
+ }
1346
+
1347
+ fn build_self(&self) {
1348
+ self.root.build_self();
1349
+ }
1350
+
1351
+ fn dispose(&self) {
1352
+ self.hide();
1353
+ self.theme_guard.borrow_mut().take();
1354
+ self.popup_presenter.dispose();
1355
+ for entry in &self.entries {
1356
+ if entry.root.handle() != crate::node::NodeHandle::INVALID {
1357
+ entry.root.dispose();
1358
+ }
1359
+ }
1360
+ for separator in &self.separators {
1361
+ if separator.root.handle() != crate::node::NodeHandle::INVALID {
1362
+ separator.root.dispose();
1363
+ }
1364
+ }
1365
+ self.root.dispose();
1366
+ }
1367
+ }