@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,467 @@
1
+ use crate::drag_drop::{DragDropEffects, DropProposal};
2
+ use crate::file::{register_browser_file, BrowserFile};
3
+ use crate::node::NodeRef;
4
+ use std::cell::RefCell;
5
+ #[cfg(feature = "native-runtime")]
6
+ use std::path::Path;
7
+
8
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
9
+ pub(crate) enum ExternalDragEventType {
10
+ Enter = 1,
11
+ Over = 2,
12
+ Leave = 3,
13
+ Drop = 4,
14
+ Unknown,
15
+ }
16
+
17
+ impl ExternalDragEventType {
18
+ pub(crate) fn from_raw(value: u32) -> Self {
19
+ match value {
20
+ 1 => Self::Enter,
21
+ 2 => Self::Over,
22
+ 3 => Self::Leave,
23
+ 4 => Self::Drop,
24
+ _ => Self::Unknown,
25
+ }
26
+ }
27
+ }
28
+
29
+ #[repr(u32)]
30
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
31
+ pub enum ExternalDropItemKind {
32
+ File = 1,
33
+ Text = 2,
34
+ Uri = 3,
35
+ Unknown(u32),
36
+ }
37
+
38
+ impl ExternalDropItemKind {
39
+ pub(crate) fn from_raw(value: u32) -> Self {
40
+ match value {
41
+ 1 => Self::File,
42
+ 2 => Self::Text,
43
+ 3 => Self::Uri,
44
+ _ => Self::Unknown(value),
45
+ }
46
+ }
47
+ }
48
+
49
+ #[derive(Clone, Debug)]
50
+ pub struct ExternalDropItemInfo {
51
+ pub id: String,
52
+ pub kind: ExternalDropItemKind,
53
+ pub name: String,
54
+ pub mime_type: Option<String>,
55
+ pub size_bytes: f64,
56
+ pub file: Option<BrowserFile>,
57
+ }
58
+
59
+ impl ExternalDropItemInfo {
60
+ pub fn new(
61
+ id: impl Into<String>,
62
+ kind: ExternalDropItemKind,
63
+ name: impl Into<String>,
64
+ mime_type: Option<String>,
65
+ size_bytes: f64,
66
+ file: Option<BrowserFile>,
67
+ ) -> Self {
68
+ Self {
69
+ id: id.into(),
70
+ kind,
71
+ name: name.into(),
72
+ mime_type,
73
+ size_bytes,
74
+ file,
75
+ }
76
+ }
77
+
78
+ #[cfg(feature = "native-runtime")]
79
+ pub fn native_path(&self) -> Option<&Path> {
80
+ (self.kind == ExternalDropItemKind::File).then(|| Path::new(&self.id))
81
+ }
82
+ }
83
+
84
+ #[derive(Clone, Debug)]
85
+ pub struct ExternalDropEventArgs {
86
+ pub x: f32,
87
+ pub y: f32,
88
+ pub modifiers: u32,
89
+ pub items: Vec<ExternalDropItemInfo>,
90
+ }
91
+
92
+ impl ExternalDropEventArgs {
93
+ pub fn new(x: f32, y: f32, modifiers: u32, items: Vec<ExternalDropItemInfo>) -> Self {
94
+ Self {
95
+ x,
96
+ y,
97
+ modifiers,
98
+ items,
99
+ }
100
+ }
101
+ }
102
+
103
+ #[derive(Default)]
104
+ struct ExternalDropState {
105
+ active_target: Option<NodeRef>,
106
+ active_effect: DragDropEffects,
107
+ }
108
+
109
+ thread_local! {
110
+ static STATE: RefCell<ExternalDropState> = RefCell::new(ExternalDropState::default());
111
+ }
112
+
113
+ fn is_default_proposal(proposal: DropProposal) -> bool {
114
+ proposal.effect == DragDropEffects::None && !proposal.show_insertion_marker
115
+ }
116
+
117
+ fn normalize_effect(candidate: DragDropEffects) -> DragDropEffects {
118
+ let masked = (candidate as u32)
119
+ & ((DragDropEffects::Copy as u32)
120
+ | (DragDropEffects::Move as u32)
121
+ | (DragDropEffects::Link as u32));
122
+ if masked == DragDropEffects::None as u32 {
123
+ return DragDropEffects::None;
124
+ }
125
+ if (masked & DragDropEffects::Move as u32) != 0 {
126
+ return DragDropEffects::Move;
127
+ }
128
+ if (masked & DragDropEffects::Copy as u32) != 0 {
129
+ return DragDropEffects::Copy;
130
+ }
131
+ if (masked & DragDropEffects::Link as u32) != 0 {
132
+ return DragDropEffects::Link;
133
+ }
134
+ DragDropEffects::None
135
+ }
136
+
137
+ fn resolve_drop_target(pointed_node: Option<NodeRef>) -> Option<NodeRef> {
138
+ let mut current = pointed_node;
139
+ while let Some(node) = current {
140
+ if node.allows_external_drop() {
141
+ return Some(node);
142
+ }
143
+ current = node.parent();
144
+ }
145
+ None
146
+ }
147
+
148
+ fn finish(
149
+ state: &mut ExternalDropState,
150
+ x: f32,
151
+ y: f32,
152
+ modifiers: u32,
153
+ items: &[ExternalDropItemInfo],
154
+ notify_target_leave: bool,
155
+ ) {
156
+ let target = state.active_target.take();
157
+ state.active_effect = DragDropEffects::None;
158
+ if notify_target_leave {
159
+ if let Some(target) = target {
160
+ target.handle_external_drag_leave(ExternalDropEventArgs::new(
161
+ x,
162
+ y,
163
+ modifiers,
164
+ items.to_vec(),
165
+ ));
166
+ }
167
+ }
168
+ }
169
+
170
+ pub(crate) fn handle_event(
171
+ pointed_node: Option<NodeRef>,
172
+ event_type: ExternalDragEventType,
173
+ x: f32,
174
+ y: f32,
175
+ modifiers: u32,
176
+ items: Vec<ExternalDropItemInfo>,
177
+ ) -> DragDropEffects {
178
+ STATE.with(|slot| {
179
+ let mut state = slot.borrow_mut();
180
+ if event_type == ExternalDragEventType::Leave {
181
+ finish(&mut state, x, y, modifiers, &items, true);
182
+ return DragDropEffects::None;
183
+ }
184
+
185
+ let target = resolve_drop_target(pointed_node);
186
+ let args = ExternalDropEventArgs::new(x, y, modifiers, items.clone());
187
+ let mut proposal = DropProposal::none();
188
+ let target_changed = match (&target, &state.active_target) {
189
+ (Some(target), Some(active)) => target.handle() != active.handle(),
190
+ (None, None) => false,
191
+ _ => true,
192
+ };
193
+ if target_changed {
194
+ if let Some(previous_target) = state.active_target.take() {
195
+ previous_target.handle_external_drag_leave(args.clone());
196
+ }
197
+ state.active_target = target.clone();
198
+ state.active_effect = DragDropEffects::None;
199
+ if let Some(target) = target.as_ref() {
200
+ if target.has_external_drag_enter_handler() {
201
+ proposal = target.handle_external_drag_enter(args.clone());
202
+ }
203
+ }
204
+ }
205
+
206
+ let Some(target) = target else {
207
+ state.active_effect = DragDropEffects::None;
208
+ return DragDropEffects::None;
209
+ };
210
+
211
+ if target.has_external_drag_over_handler() {
212
+ proposal = target.handle_external_drag_over(args.clone());
213
+ } else if is_default_proposal(proposal) {
214
+ proposal = DropProposal::new(state.active_effect, false);
215
+ }
216
+
217
+ let effect = normalize_effect(proposal.effect);
218
+ state.active_effect = effect;
219
+ if event_type == ExternalDragEventType::Drop {
220
+ if effect != DragDropEffects::None {
221
+ target.handle_external_drop_event(args);
222
+ }
223
+ finish(&mut state, x, y, modifiers, &items, true);
224
+ }
225
+ effect
226
+ })
227
+ }
228
+
229
+ pub(crate) fn handle_node_destroyed(node: NodeRef) {
230
+ STATE.with(|slot| {
231
+ let mut state = slot.borrow_mut();
232
+ let Some(active_target) = state.active_target.as_ref() else {
233
+ return;
234
+ };
235
+ if active_target.handle() == node.handle() {
236
+ state.active_target = None;
237
+ state.active_effect = DragDropEffects::None;
238
+ }
239
+ });
240
+ }
241
+
242
+ pub(crate) fn reset() {
243
+ STATE.with(|slot| {
244
+ let mut state = slot.borrow_mut();
245
+ state.active_target = None;
246
+ state.active_effect = DragDropEffects::None;
247
+ });
248
+ }
249
+
250
+ pub(crate) fn decode_payload(
251
+ payload_ptr: *const u8,
252
+ payload_len: u32,
253
+ ) -> Vec<ExternalDropItemInfo> {
254
+ let mut items = Vec::new();
255
+ if payload_ptr.is_null() || payload_len == 0 {
256
+ return items;
257
+ }
258
+ let bytes = unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) };
259
+ if bytes.len() < 4 {
260
+ crate::logger::warn("ExternalDrop", "Malformed external drop payload header.");
261
+ return items;
262
+ }
263
+ let mut cursor = 0usize;
264
+ let item_count = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4]));
265
+ cursor += 4;
266
+ for index in 0..item_count {
267
+ if cursor + 12 > bytes.len() {
268
+ crate::logger::warn(
269
+ "ExternalDrop",
270
+ &format!("Truncated external drop item header at index {}.", index),
271
+ );
272
+ return items;
273
+ }
274
+ let kind = ExternalDropItemKind::from_raw(u32::from_le_bytes(
275
+ bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4]),
276
+ ));
277
+ cursor += 4;
278
+ let size_bytes = f64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
279
+ cursor += 8;
280
+
281
+ let Some(id) = decode_string(bytes, &mut cursor, "id", index) else {
282
+ return items;
283
+ };
284
+ let Some(name) = decode_string(bytes, &mut cursor, "name", index) else {
285
+ return items;
286
+ };
287
+ let Some(mime_type) = decode_string(bytes, &mut cursor, "mime", index) else {
288
+ return items;
289
+ };
290
+ let mime_type = if mime_type.is_empty() {
291
+ None
292
+ } else {
293
+ Some(mime_type)
294
+ };
295
+ let file = if kind == ExternalDropItemKind::File && !id.is_empty() {
296
+ Some(register_browser_file(
297
+ id.clone(),
298
+ name.clone(),
299
+ mime_type.clone(),
300
+ size_bytes as u64,
301
+ 0,
302
+ ))
303
+ } else {
304
+ None
305
+ };
306
+ items.push(ExternalDropItemInfo::new(
307
+ id, kind, name, mime_type, size_bytes, file,
308
+ ));
309
+ }
310
+ items
311
+ }
312
+
313
+ fn decode_string(bytes: &[u8], cursor: &mut usize, label: &str, index: u32) -> Option<String> {
314
+ if *cursor + 4 > bytes.len() {
315
+ crate::logger::warn(
316
+ "ExternalDrop",
317
+ &format!(
318
+ "Truncated external drop item {} length at index {}.",
319
+ label, index
320
+ ),
321
+ );
322
+ return None;
323
+ }
324
+ let len = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().unwrap_or([0; 4])) as usize;
325
+ *cursor += 4;
326
+ if *cursor + len > bytes.len() {
327
+ crate::logger::warn(
328
+ "ExternalDrop",
329
+ &format!("Truncated external drop item {} at index {}.", label, index),
330
+ );
331
+ return None;
332
+ }
333
+ let value = String::from_utf8_lossy(&bytes[*cursor..*cursor + len]).into_owned();
334
+ *cursor += len;
335
+ Some(value)
336
+ }
337
+
338
+ #[cfg(test)]
339
+ mod tests {
340
+ use super::*;
341
+ use crate::drag_drop::DropProposal;
342
+ use crate::event::reset as reset_events;
343
+ use crate::node::{flex_box, Node};
344
+ use std::cell::{Cell, RefCell};
345
+ use std::rc::Rc;
346
+
347
+ #[test]
348
+ fn routes_external_drop_enter_over_leave_and_drop() {
349
+ reset();
350
+ reset_events();
351
+
352
+ let root = flex_box();
353
+ let target = flex_box();
354
+ root.child(&target);
355
+
356
+ let enter_count = Rc::new(Cell::new(0));
357
+ let over_count = Rc::new(Cell::new(0));
358
+ let leave_count = Rc::new(Cell::new(0));
359
+ let drop_count = Rc::new(Cell::new(0));
360
+ let last_name = Rc::new(RefCell::new(String::new()));
361
+
362
+ target
363
+ .allow_external_drop(true)
364
+ .on_external_drag_enter({
365
+ let enter_count = enter_count.clone();
366
+ let last_name = last_name.clone();
367
+ move |args| {
368
+ enter_count.set(enter_count.get() + 1);
369
+ if let Some(item) = args.items.first() {
370
+ last_name.replace(item.name.clone());
371
+ }
372
+ DropProposal::new(DragDropEffects::Copy, false)
373
+ }
374
+ })
375
+ .on_external_drag_over({
376
+ let over_count = over_count.clone();
377
+ move |_args| {
378
+ over_count.set(over_count.get() + 1);
379
+ DropProposal::new(DragDropEffects::Copy, false)
380
+ }
381
+ })
382
+ .on_external_drag_leave({
383
+ let leave_count = leave_count.clone();
384
+ move |_args| {
385
+ leave_count.set(leave_count.get() + 1);
386
+ }
387
+ })
388
+ .on_external_drop({
389
+ let drop_count = drop_count.clone();
390
+ move |_args| {
391
+ drop_count.set(drop_count.get() + 1);
392
+ }
393
+ });
394
+
395
+ let items = vec![ExternalDropItemInfo::new(
396
+ "external-drop-1",
397
+ ExternalDropItemKind::File,
398
+ "todo.txt",
399
+ Some("text/plain".to_string()),
400
+ 10.0,
401
+ None,
402
+ )];
403
+
404
+ assert_eq!(
405
+ handle_event(
406
+ Some(target.node_ref()),
407
+ ExternalDragEventType::Enter,
408
+ 12.0,
409
+ 18.0,
410
+ 0,
411
+ items.clone(),
412
+ ),
413
+ DragDropEffects::Copy
414
+ );
415
+ assert_eq!(
416
+ handle_event(
417
+ Some(target.node_ref()),
418
+ ExternalDragEventType::Over,
419
+ 14.0,
420
+ 19.0,
421
+ 0,
422
+ items.clone(),
423
+ ),
424
+ DragDropEffects::Copy
425
+ );
426
+ assert_eq!(
427
+ handle_event(
428
+ Some(target.node_ref()),
429
+ ExternalDragEventType::Drop,
430
+ 16.0,
431
+ 20.0,
432
+ 0,
433
+ items,
434
+ ),
435
+ DragDropEffects::Copy
436
+ );
437
+
438
+ assert_eq!(enter_count.get(), 1);
439
+ assert_eq!(over_count.get(), 3);
440
+ assert_eq!(leave_count.get(), 1);
441
+ assert_eq!(drop_count.get(), 1);
442
+ assert_eq!(last_name.borrow().as_str(), "todo.txt");
443
+ }
444
+
445
+ #[test]
446
+ fn decodes_unknown_external_item_kind_without_registering_file() {
447
+ reset();
448
+ reset_events();
449
+
450
+ let mut payload = Vec::new();
451
+ payload.extend_from_slice(&1u32.to_le_bytes());
452
+ payload.extend_from_slice(&99u32.to_le_bytes());
453
+ payload.extend_from_slice(&123.0f64.to_le_bytes());
454
+ for value in ["external-drop-unknown", "note.txt", "text/plain"] {
455
+ payload.extend_from_slice(&(value.len() as u32).to_le_bytes());
456
+ payload.extend_from_slice(value.as_bytes());
457
+ }
458
+
459
+ let items = decode_payload(payload.as_ptr(), payload.len() as u32);
460
+ assert_eq!(items.len(), 1);
461
+ assert_eq!(items[0].kind, ExternalDropItemKind::Unknown(99));
462
+ assert_eq!(items[0].id, "external-drop-unknown");
463
+ assert_eq!(items[0].name, "note.txt");
464
+ assert_eq!(items[0].mime_type.as_deref(), Some("text/plain"));
465
+ assert!(items[0].file.is_none());
466
+ }
467
+ }