@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,556 @@
1
+ use crate::ffi;
2
+ use crate::generated::framework_host_services;
3
+
4
+ #[cfg(feature = "native-runtime")]
5
+ use std::cell::RefCell;
6
+ #[cfg(feature = "native-runtime")]
7
+ use std::collections::HashMap;
8
+ #[cfg(feature = "native-runtime")]
9
+ use std::path::Path;
10
+ #[cfg(feature = "native-runtime")]
11
+ use std::path::PathBuf;
12
+ #[cfg(feature = "native-runtime")]
13
+ use std::sync::atomic::{AtomicU64, Ordering};
14
+
15
+ #[cfg(feature = "native-runtime")]
16
+ unsafe extern "C" {
17
+ fn fui_dispatch_to_ui(callback_id: u64) -> bool;
18
+ fn fui_cancel_ui_dispatch_async(callback_id: u64) -> bool;
19
+ fn fui_native_clipboard_write(text: *const u8, length: u32) -> bool;
20
+ fn fui_native_clipboard_text_length() -> u32;
21
+ fn fui_native_clipboard_copy(destination: *mut u8, capacity: u32) -> u32;
22
+ fn fui_native_open_external_url(value: *const u8, length: u32) -> bool;
23
+ fn fui_native_open_file(value: *const u8, length: u32) -> bool;
24
+ fn fui_native_reveal_file(value: *const u8, length: u32) -> bool;
25
+ fn fui_native_show_file_dialog(
26
+ kind: u32,
27
+ request_id: u64,
28
+ filters: *const u8,
29
+ filters_length: u32,
30
+ default_location: *const u8,
31
+ default_location_length: u32,
32
+ allow_multiple: bool,
33
+ ) -> bool;
34
+ }
35
+
36
+ #[cfg(feature = "native-runtime")]
37
+ thread_local! {
38
+ static UI_DISPATCH_CALLBACKS: RefCell<HashMap<u64, Box<dyn FnOnce()>>> = RefCell::new(HashMap::new());
39
+ }
40
+
41
+ #[cfg(feature = "native-runtime")]
42
+ static NEXT_UI_DISPATCH_ID: AtomicU64 = AtomicU64::new(1);
43
+
44
+ #[cfg(feature = "native-runtime")]
45
+ static NEXT_NATIVE_FILE_DIALOG_ID: AtomicU64 = AtomicU64::new(1);
46
+
47
+ #[cfg(feature = "native-runtime")]
48
+ type NativeFileDialogCallback = Box<dyn FnOnce(NativeFileDialogResult)>;
49
+
50
+ #[cfg(feature = "native-runtime")]
51
+ thread_local! {
52
+ static NATIVE_FILE_DIALOG_CALLBACKS: RefCell<HashMap<u64, NativeFileDialogCallback>> = RefCell::new(HashMap::new());
53
+ }
54
+
55
+ #[cfg(feature = "native-runtime")]
56
+ #[derive(Clone, Debug, PartialEq, Eq)]
57
+ pub struct NativeFileFilter {
58
+ pub name: String,
59
+ pub extensions: Vec<String>,
60
+ }
61
+
62
+ #[cfg(feature = "native-runtime")]
63
+ impl NativeFileFilter {
64
+ pub fn new(
65
+ name: impl Into<String>,
66
+ extensions: impl IntoIterator<Item = impl Into<String>>,
67
+ ) -> Self {
68
+ Self {
69
+ name: name.into(),
70
+ extensions: extensions.into_iter().map(Into::into).collect(),
71
+ }
72
+ }
73
+ }
74
+
75
+ #[cfg(feature = "native-runtime")]
76
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
77
+ pub struct NativeFileDialogOptions {
78
+ pub filters: Vec<NativeFileFilter>,
79
+ pub default_location: Option<PathBuf>,
80
+ pub allow_multiple: bool,
81
+ }
82
+
83
+ #[cfg(feature = "native-runtime")]
84
+ #[derive(Clone, Debug, PartialEq, Eq)]
85
+ pub enum NativeFileDialogResult {
86
+ Selected {
87
+ paths: Vec<PathBuf>,
88
+ selected_filter: Option<usize>,
89
+ },
90
+ Cancelled,
91
+ Error(String),
92
+ }
93
+
94
+ #[cfg(feature = "native-runtime")]
95
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
96
+ pub struct NativeFileDialogRequest {
97
+ request_id: u64,
98
+ }
99
+
100
+ #[cfg(feature = "native-runtime")]
101
+ impl NativeFileDialogRequest {
102
+ pub fn id(self) -> u64 {
103
+ self.request_id
104
+ }
105
+ }
106
+
107
+ /// A one-shot, `Send` token for work whose closure remains owned by the UI thread.
108
+ #[cfg(feature = "native-runtime")]
109
+ pub struct UiDispatchHandle {
110
+ callback_id: u64,
111
+ pending: bool,
112
+ }
113
+
114
+ #[cfg(feature = "native-runtime")]
115
+ impl UiDispatchHandle {
116
+ pub fn dispatch(mut self) -> bool {
117
+ let dispatched = unsafe { fui_dispatch_to_ui(self.callback_id) };
118
+ if dispatched {
119
+ self.pending = false;
120
+ }
121
+ dispatched
122
+ }
123
+ }
124
+
125
+ #[cfg(feature = "native-runtime")]
126
+ impl Drop for UiDispatchHandle {
127
+ fn drop(&mut self) {
128
+ if self.pending {
129
+ unsafe {
130
+ fui_cancel_ui_dispatch_async(self.callback_id);
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ #[cfg(feature = "native-runtime")]
137
+ pub struct UiDispatcher;
138
+
139
+ #[cfg(feature = "native-runtime")]
140
+ impl UiDispatcher {
141
+ /// Keeps retained work on the UI thread and returns a token that may be sent to a worker.
142
+ pub fn prepare(callback: impl FnOnce() + 'static) -> UiDispatchHandle {
143
+ let callback_id = NEXT_UI_DISPATCH_ID.fetch_add(1, Ordering::Relaxed);
144
+ UI_DISPATCH_CALLBACKS.with(|callbacks| {
145
+ callbacks
146
+ .borrow_mut()
147
+ .insert(callback_id, Box::new(callback));
148
+ });
149
+ UiDispatchHandle {
150
+ callback_id,
151
+ pending: true,
152
+ }
153
+ }
154
+ }
155
+
156
+ #[cfg(feature = "native-runtime")]
157
+ pub fn write_clipboard_text(text: &str) -> bool {
158
+ unsafe { fui_native_clipboard_write(text.as_ptr(), text.len() as u32) }
159
+ }
160
+
161
+ #[cfg(feature = "native-runtime")]
162
+ pub fn read_clipboard_text() -> Option<String> {
163
+ let length = unsafe { fui_native_clipboard_text_length() };
164
+ let mut bytes = vec![0u8; length as usize];
165
+ let copied = unsafe { fui_native_clipboard_copy(bytes.as_mut_ptr(), length) };
166
+ bytes.truncate(copied as usize);
167
+ String::from_utf8(bytes).ok()
168
+ }
169
+
170
+ #[cfg(feature = "native-runtime")]
171
+ pub fn open_external_url(url: &str) -> bool {
172
+ unsafe { fui_native_open_external_url(url.as_ptr(), url.len() as u32) }
173
+ }
174
+
175
+ #[cfg(feature = "native-runtime")]
176
+ pub fn open_file(path: impl AsRef<Path>) -> bool {
177
+ let Some(path) = path.as_ref().to_str() else {
178
+ return false;
179
+ };
180
+ unsafe { fui_native_open_file(path.as_ptr(), path.len() as u32) }
181
+ }
182
+
183
+ #[cfg(feature = "native-runtime")]
184
+ pub fn reveal_file(path: impl AsRef<Path>) -> bool {
185
+ let Some(path) = path.as_ref().to_str() else {
186
+ return false;
187
+ };
188
+ unsafe { fui_native_reveal_file(path.as_ptr(), path.len() as u32) }
189
+ }
190
+
191
+ #[cfg(feature = "native-runtime")]
192
+ fn show_native_file_dialog(
193
+ kind: u32,
194
+ options: NativeFileDialogOptions,
195
+ callback: impl FnOnce(NativeFileDialogResult) + 'static,
196
+ ) -> Option<NativeFileDialogRequest> {
197
+ let request_id = NEXT_NATIVE_FILE_DIALOG_ID.fetch_add(1, Ordering::Relaxed);
198
+ let mut encoded_filters = Vec::new();
199
+ for filter in &options.filters {
200
+ if filter.name.is_empty() || filter.extensions.is_empty() {
201
+ return None;
202
+ }
203
+ encoded_filters.extend_from_slice(filter.name.as_bytes());
204
+ encoded_filters.push(0);
205
+ encoded_filters.extend_from_slice(filter.extensions.join(";").as_bytes());
206
+ encoded_filters.push(0);
207
+ }
208
+ let default_location = options
209
+ .default_location
210
+ .as_ref()
211
+ .and_then(|path| path.to_str())
212
+ .unwrap_or_default();
213
+ NATIVE_FILE_DIALOG_CALLBACKS.with(|callbacks| {
214
+ callbacks
215
+ .borrow_mut()
216
+ .insert(request_id, Box::new(callback));
217
+ });
218
+ let shown = unsafe {
219
+ fui_native_show_file_dialog(
220
+ kind,
221
+ request_id,
222
+ encoded_filters.as_ptr(),
223
+ encoded_filters.len() as u32,
224
+ default_location.as_ptr(),
225
+ default_location.len() as u32,
226
+ options.allow_multiple,
227
+ )
228
+ };
229
+ if !shown {
230
+ NATIVE_FILE_DIALOG_CALLBACKS.with(|callbacks| {
231
+ callbacks.borrow_mut().remove(&request_id);
232
+ });
233
+ return None;
234
+ }
235
+ Some(NativeFileDialogRequest { request_id })
236
+ }
237
+
238
+ #[cfg(feature = "native-runtime")]
239
+ pub fn show_open_file_dialog(
240
+ options: NativeFileDialogOptions,
241
+ callback: impl FnOnce(NativeFileDialogResult) + 'static,
242
+ ) -> Option<NativeFileDialogRequest> {
243
+ show_native_file_dialog(0, options, callback)
244
+ }
245
+
246
+ #[cfg(feature = "native-runtime")]
247
+ pub fn show_save_file_dialog(
248
+ options: NativeFileDialogOptions,
249
+ callback: impl FnOnce(NativeFileDialogResult) + 'static,
250
+ ) -> Option<NativeFileDialogRequest> {
251
+ show_native_file_dialog(1, options, callback)
252
+ }
253
+
254
+ #[cfg(feature = "native-runtime")]
255
+ pub fn show_open_folder_dialog(
256
+ options: NativeFileDialogOptions,
257
+ callback: impl FnOnce(NativeFileDialogResult) + 'static,
258
+ ) -> Option<NativeFileDialogRequest> {
259
+ show_native_file_dialog(2, options, callback)
260
+ }
261
+
262
+ #[cfg(feature = "native-runtime")]
263
+ #[no_mangle]
264
+ pub unsafe extern "C" fn __fui_complete_native_file_dialog(
265
+ request_id: u64,
266
+ status: u32,
267
+ payload: *const u8,
268
+ payload_length: u32,
269
+ selected_filter: i32,
270
+ ) -> bool {
271
+ let callback =
272
+ NATIVE_FILE_DIALOG_CALLBACKS.with(|callbacks| callbacks.borrow_mut().remove(&request_id));
273
+ let Some(callback) = callback else {
274
+ return false;
275
+ };
276
+ let bytes = if payload.is_null() || payload_length == 0 {
277
+ &[][..]
278
+ } else {
279
+ unsafe { std::slice::from_raw_parts(payload, payload_length as usize) }
280
+ };
281
+ let result = match status {
282
+ 0 => NativeFileDialogResult::Selected {
283
+ paths: bytes
284
+ .split(|byte| *byte == 0)
285
+ .filter(|path| !path.is_empty())
286
+ .filter_map(|path| std::str::from_utf8(path).ok())
287
+ .map(PathBuf::from)
288
+ .collect(),
289
+ selected_filter: usize::try_from(selected_filter).ok(),
290
+ },
291
+ 1 => NativeFileDialogResult::Cancelled,
292
+ _ => NativeFileDialogResult::Error(String::from_utf8_lossy(bytes).into_owned()),
293
+ };
294
+ callback(result);
295
+ true
296
+ }
297
+
298
+ #[cfg(feature = "native-runtime")]
299
+ #[no_mangle]
300
+ pub extern "C" fn __fui_clear_native_file_dialog_callbacks() {
301
+ NATIVE_FILE_DIALOG_CALLBACKS.with(|callbacks| callbacks.borrow_mut().clear());
302
+ }
303
+
304
+ #[cfg(feature = "native-runtime")]
305
+ #[no_mangle]
306
+ pub extern "C" fn __fui_run_ui_dispatch(callback_id: u64) -> bool {
307
+ let callback =
308
+ UI_DISPATCH_CALLBACKS.with(|callbacks| callbacks.borrow_mut().remove(&callback_id));
309
+ let Some(callback) = callback else {
310
+ return false;
311
+ };
312
+ callback();
313
+ true
314
+ }
315
+
316
+ #[cfg(feature = "native-runtime")]
317
+ #[no_mangle]
318
+ pub extern "C" fn __fui_cancel_ui_dispatch(callback_id: u64) {
319
+ UI_DISPATCH_CALLBACKS.with(|callbacks| {
320
+ callbacks.borrow_mut().remove(&callback_id);
321
+ });
322
+ }
323
+
324
+ #[cfg(feature = "native-runtime")]
325
+ #[no_mangle]
326
+ pub extern "C" fn __fui_clear_ui_dispatches() {
327
+ UI_DISPATCH_CALLBACKS.with(|callbacks| callbacks.borrow_mut().clear());
328
+ }
329
+
330
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
331
+ pub enum PlatformFamily {
332
+ Unknown = 0,
333
+ Apple = 1,
334
+ Windows = 2,
335
+ Linux = 3,
336
+ }
337
+
338
+ pub fn device_pixel_ratio() -> f32 {
339
+ unsafe { ffi::get_device_pixel_ratio() }
340
+ }
341
+
342
+ pub fn platform_family() -> PlatformFamily {
343
+ match framework_host_services::fui_get_platform_family() {
344
+ 1 => PlatformFamily::Apple,
345
+ 2 => PlatformFamily::Windows,
346
+ 3 => PlatformFamily::Linux,
347
+ _ => PlatformFamily::Unknown,
348
+ }
349
+ }
350
+
351
+ pub fn is_coarse_pointer() -> bool {
352
+ framework_host_services::fui_is_coarse_pointer()
353
+ }
354
+
355
+ pub fn primary_shortcut_modifier() -> u32 {
356
+ match platform_family() {
357
+ PlatformFamily::Apple => ffi::KeyModifier::Meta as u32,
358
+ _ => ffi::KeyModifier::Ctrl as u32,
359
+ }
360
+ }
361
+
362
+ pub fn word_navigation_modifier() -> u32 {
363
+ match platform_family() {
364
+ PlatformFamily::Apple => ffi::KeyModifier::Alt as u32,
365
+ _ => ffi::KeyModifier::Ctrl as u32,
366
+ }
367
+ }
368
+
369
+ pub fn line_boundary_modifier() -> u32 {
370
+ match platform_family() {
371
+ PlatformFamily::Apple => ffi::KeyModifier::Meta as u32,
372
+ _ => 0,
373
+ }
374
+ }
375
+
376
+ pub fn document_boundary_modifier() -> u32 {
377
+ match platform_family() {
378
+ PlatformFamily::Apple => ffi::KeyModifier::Meta as u32,
379
+ _ => ffi::KeyModifier::Ctrl as u32,
380
+ }
381
+ }
382
+
383
+ fn has_modifier(modifiers: u32, expected: u32) -> bool {
384
+ expected != 0 && (modifiers & expected) != 0
385
+ }
386
+
387
+ pub fn has_primary_shortcut_modifier(modifiers: u32) -> bool {
388
+ has_modifier(modifiers, primary_shortcut_modifier())
389
+ }
390
+
391
+ pub fn has_word_navigation_modifier(modifiers: u32) -> bool {
392
+ has_modifier(modifiers, word_navigation_modifier())
393
+ }
394
+
395
+ pub fn has_line_boundary_modifier(modifiers: u32) -> bool {
396
+ has_modifier(modifiers, line_boundary_modifier())
397
+ }
398
+
399
+ pub fn has_document_boundary_modifier(modifiers: u32) -> bool {
400
+ has_modifier(modifiers, document_boundary_modifier())
401
+ }
402
+
403
+ fn format_shortcut_key_token(key: &str, platform_family: PlatformFamily) -> String {
404
+ match key {
405
+ "ArrowLeft" => {
406
+ if platform_family == PlatformFamily::Apple {
407
+ "←".to_string()
408
+ } else {
409
+ "Left".to_string()
410
+ }
411
+ }
412
+ "ArrowRight" => {
413
+ if platform_family == PlatformFamily::Apple {
414
+ "→".to_string()
415
+ } else {
416
+ "Right".to_string()
417
+ }
418
+ }
419
+ "ArrowUp" => {
420
+ if platform_family == PlatformFamily::Apple {
421
+ "↑".to_string()
422
+ } else {
423
+ "Up".to_string()
424
+ }
425
+ }
426
+ "ArrowDown" => {
427
+ if platform_family == PlatformFamily::Apple {
428
+ "↓".to_string()
429
+ } else {
430
+ "Down".to_string()
431
+ }
432
+ }
433
+ "PageUp" => "PgUp".to_string(),
434
+ "PageDown" => "PgDn".to_string(),
435
+ _ if key.chars().count() == 1 => key.to_uppercase(),
436
+ _ => key.to_string(),
437
+ }
438
+ }
439
+
440
+ fn append_shortcut_modifier_tokens(
441
+ tokens: &mut Vec<String>,
442
+ modifiers: u32,
443
+ platform: PlatformFamily,
444
+ ) {
445
+ if platform == PlatformFamily::Apple {
446
+ if (modifiers & ffi::KeyModifier::Ctrl as u32) != 0 {
447
+ tokens.push("⌃".to_string());
448
+ }
449
+ if (modifiers & ffi::KeyModifier::Alt as u32) != 0 {
450
+ tokens.push("⌥".to_string());
451
+ }
452
+ if (modifiers & ffi::KeyModifier::Shift as u32) != 0 {
453
+ tokens.push("⇧".to_string());
454
+ }
455
+ if (modifiers & ffi::KeyModifier::Meta as u32) != 0 {
456
+ tokens.push("⌘".to_string());
457
+ }
458
+ return;
459
+ }
460
+
461
+ if (modifiers & ffi::KeyModifier::Ctrl as u32) != 0 {
462
+ tokens.push("Ctrl".to_string());
463
+ }
464
+ if (modifiers & ffi::KeyModifier::Alt as u32) != 0 {
465
+ tokens.push("Alt".to_string());
466
+ }
467
+ if (modifiers & ffi::KeyModifier::Shift as u32) != 0 {
468
+ tokens.push("Shift".to_string());
469
+ }
470
+ if (modifiers & ffi::KeyModifier::Meta as u32) != 0 {
471
+ tokens.push("Meta".to_string());
472
+ }
473
+ }
474
+
475
+ pub fn format_shortcut_label(key: &str, modifiers: u32) -> String {
476
+ let platform = platform_family();
477
+ let mut tokens = Vec::new();
478
+ append_shortcut_modifier_tokens(&mut tokens, modifiers, platform);
479
+ tokens.push(format_shortcut_key_token(key, platform));
480
+ if platform == PlatformFamily::Apple {
481
+ tokens.join("")
482
+ } else {
483
+ tokens.join("+")
484
+ }
485
+ }
486
+
487
+ pub fn format_primary_shortcut_label(key: &str) -> String {
488
+ format_shortcut_label(key, primary_shortcut_modifier())
489
+ }
490
+
491
+ pub fn format_undo_shortcut_label() -> String {
492
+ format_primary_shortcut_label("z")
493
+ }
494
+
495
+ pub fn format_redo_shortcut_label() -> String {
496
+ match platform_family() {
497
+ PlatformFamily::Apple => format_shortcut_label(
498
+ "z",
499
+ primary_shortcut_modifier() | ffi::KeyModifier::Shift as u32,
500
+ ),
501
+ _ => format_primary_shortcut_label("y"),
502
+ }
503
+ }
504
+
505
+ fn matches_shortcut_key(key: &str, expected: &str) -> bool {
506
+ key.eq_ignore_ascii_case(expected)
507
+ }
508
+
509
+ pub fn is_undo_shortcut(key: &str, modifiers: u32) -> bool {
510
+ (modifiers & ffi::KeyModifier::Shift as u32) == 0
511
+ && has_primary_shortcut_modifier(modifiers)
512
+ && matches_shortcut_key(key, "z")
513
+ }
514
+
515
+ pub fn is_redo_shortcut(key: &str, modifiers: u32) -> bool {
516
+ match platform_family() {
517
+ PlatformFamily::Apple => {
518
+ has_primary_shortcut_modifier(modifiers)
519
+ && (modifiers & ffi::KeyModifier::Shift as u32) != 0
520
+ && matches_shortcut_key(key, "z")
521
+ }
522
+ _ => has_primary_shortcut_modifier(modifiers) && matches_shortcut_key(key, "y"),
523
+ }
524
+ }
525
+
526
+ #[cfg(test)]
527
+ mod tests {
528
+ use super::{
529
+ document_boundary_modifier, is_coarse_pointer, is_redo_shortcut, is_undo_shortcut,
530
+ line_boundary_modifier, platform_family, PlatformFamily,
531
+ };
532
+ use crate::ffi;
533
+
534
+ #[test]
535
+ fn returns_mock_device_pixel_ratio() {
536
+ ffi::test::reset();
537
+ ffi::test::set_device_pixel_ratio(2.5);
538
+ assert_eq!(super::device_pixel_ratio(), 2.5);
539
+ }
540
+
541
+ #[test]
542
+ fn reports_platform_family_and_pointer_mode() {
543
+ ffi::test::reset();
544
+ ffi::test::set_platform_family(1);
545
+ ffi::test::set_coarse_pointer(true);
546
+ assert_eq!(platform_family(), PlatformFamily::Apple);
547
+ assert!(is_coarse_pointer());
548
+ assert_eq!(line_boundary_modifier(), ffi::KeyModifier::Meta as u32);
549
+ assert_eq!(document_boundary_modifier(), ffi::KeyModifier::Meta as u32);
550
+ assert!(is_undo_shortcut("z", ffi::KeyModifier::Meta as u32));
551
+ assert!(is_redo_shortcut(
552
+ "z",
553
+ ffi::KeyModifier::Meta as u32 | ffi::KeyModifier::Shift as u32
554
+ ));
555
+ }
556
+ }