@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,307 @@
1
+ use crate::bindings::ui;
2
+ use crate::ffi::{PositionType, Unit};
3
+ use crate::node::{flex_box, portal, FlexBox, Node, NodeHandle};
4
+ use crate::theme::current_theme;
5
+ use std::cell::RefCell;
6
+
7
+ const STANDARD_FOCUS_RING_WIDTH: f32 = 2.0;
8
+ const STANDARD_FOCUS_RING_OUTSET: f32 = 2.0;
9
+ const TRANSPARENT: u32 = 0x00000000;
10
+
11
+ #[derive(Clone, Copy)]
12
+ struct FocusAdornerStyle {
13
+ top_left_radius: f32,
14
+ top_right_radius: f32,
15
+ bottom_right_radius: f32,
16
+ bottom_left_radius: f32,
17
+ }
18
+
19
+ #[derive(Clone, Copy)]
20
+ struct FocusAdornerRect {
21
+ x: f32,
22
+ y: f32,
23
+ width: f32,
24
+ height: f32,
25
+ }
26
+
27
+ struct FocusAdornerState {
28
+ host_root: Option<FlexBox>,
29
+ ring_node: Option<FlexBox>,
30
+ active_owner: Option<NodeHandle>,
31
+ active_style: Option<FocusAdornerStyle>,
32
+ attached: bool,
33
+ last_host_x: f32,
34
+ last_host_y: f32,
35
+ last_host_width: f32,
36
+ last_host_height: f32,
37
+ last_ring_x: f32,
38
+ last_ring_y: f32,
39
+ last_ring_width: f32,
40
+ last_ring_height: f32,
41
+ last_color: u32,
42
+ last_top_left_radius: f32,
43
+ last_top_right_radius: f32,
44
+ last_bottom_right_radius: f32,
45
+ last_bottom_left_radius: f32,
46
+ }
47
+
48
+ impl FocusAdornerState {
49
+ fn new() -> Self {
50
+ Self {
51
+ host_root: None,
52
+ ring_node: None,
53
+ active_owner: None,
54
+ active_style: None,
55
+ attached: false,
56
+ last_host_x: f32::NAN,
57
+ last_host_y: f32::NAN,
58
+ last_host_width: f32::NAN,
59
+ last_host_height: f32::NAN,
60
+ last_ring_x: f32::NAN,
61
+ last_ring_y: f32::NAN,
62
+ last_ring_width: f32::NAN,
63
+ last_ring_height: f32::NAN,
64
+ last_color: 0,
65
+ last_top_left_radius: f32::NAN,
66
+ last_top_right_radius: f32::NAN,
67
+ last_bottom_right_radius: f32::NAN,
68
+ last_bottom_left_radius: f32::NAN,
69
+ }
70
+ }
71
+
72
+ fn reset_cached_geometry(&mut self) {
73
+ self.last_host_x = f32::NAN;
74
+ self.last_host_y = f32::NAN;
75
+ self.last_host_width = f32::NAN;
76
+ self.last_host_height = f32::NAN;
77
+ self.last_ring_x = f32::NAN;
78
+ self.last_ring_y = f32::NAN;
79
+ self.last_ring_width = f32::NAN;
80
+ self.last_ring_height = f32::NAN;
81
+ self.last_color = 0;
82
+ self.last_top_left_radius = f32::NAN;
83
+ self.last_top_right_radius = f32::NAN;
84
+ self.last_bottom_right_radius = f32::NAN;
85
+ self.last_bottom_left_radius = f32::NAN;
86
+ }
87
+ }
88
+
89
+ thread_local! {
90
+ static FOCUS_ADORNER_STATE: RefCell<FocusAdornerState> =
91
+ RefCell::new(FocusAdornerState::new());
92
+ }
93
+
94
+ pub(crate) fn create_default_host() -> FlexBox {
95
+ FOCUS_ADORNER_STATE.with(|slot| {
96
+ let mut state = slot.borrow_mut();
97
+ if let Some(existing_host) = state.host_root.as_ref() {
98
+ return existing_host.clone();
99
+ }
100
+ let ring_node = flex_box();
101
+ ring_node
102
+ .position_type(PositionType::Absolute)
103
+ .bg_color(TRANSPARENT)
104
+ .border(STANDARD_FOCUS_RING_WIDTH, TRANSPARENT);
105
+ let host_root = portal();
106
+ host_root
107
+ .position_type(PositionType::Absolute)
108
+ .position(0.0, 0.0)
109
+ .width(0.0, Unit::Pixel)
110
+ .height(0.0, Unit::Pixel)
111
+ .clip_to_bounds(false);
112
+ state.host_root = Some(host_root.clone());
113
+ state.ring_node = Some(ring_node);
114
+ host_root
115
+ })
116
+ }
117
+
118
+ pub(crate) fn clear() {
119
+ hide();
120
+ FOCUS_ADORNER_STATE.with(|slot| {
121
+ let mut state = slot.borrow_mut();
122
+ state.active_owner = None;
123
+ state.active_style = None;
124
+ });
125
+ }
126
+
127
+ pub(crate) fn show_standard<T: Node>(owner: &T, corner_radius: f32) {
128
+ show_standard_corners(
129
+ owner,
130
+ corner_radius,
131
+ corner_radius,
132
+ corner_radius,
133
+ corner_radius,
134
+ );
135
+ }
136
+
137
+ pub(crate) fn show_standard_corners<T: Node>(owner: &T, tl: f32, tr: f32, br: f32, bl: f32) {
138
+ FOCUS_ADORNER_STATE.with(|slot| {
139
+ let mut state = slot.borrow_mut();
140
+ state.active_owner = Some(owner.handle());
141
+ state.active_style = Some(FocusAdornerStyle {
142
+ top_left_radius: tl + STANDARD_FOCUS_RING_OUTSET,
143
+ top_right_radius: tr + STANDARD_FOCUS_RING_OUTSET,
144
+ bottom_right_radius: br + STANDARD_FOCUS_RING_OUTSET,
145
+ bottom_left_radius: bl + STANDARD_FOCUS_RING_OUTSET,
146
+ });
147
+ });
148
+ sync();
149
+ }
150
+
151
+ pub(crate) fn hide_owner<T: Node>(owner: &T) {
152
+ hide_owner_handle(owner.handle());
153
+ }
154
+
155
+ pub(crate) fn handle_owner_destroyed(owner: NodeHandle) {
156
+ hide_owner_handle(owner);
157
+ }
158
+
159
+ pub(crate) fn refresh_after_commit() -> bool {
160
+ sync()
161
+ }
162
+
163
+ fn hide_owner_handle(owner: NodeHandle) {
164
+ let should_hide = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_owner == Some(owner));
165
+ if !should_hide {
166
+ return;
167
+ }
168
+ FOCUS_ADORNER_STATE.with(|slot| {
169
+ let mut state = slot.borrow_mut();
170
+ state.active_owner = None;
171
+ state.active_style = None;
172
+ });
173
+ hide();
174
+ }
175
+
176
+ fn sync() -> bool {
177
+ let owner = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_owner);
178
+ let style = FOCUS_ADORNER_STATE.with(|slot| slot.borrow().active_style);
179
+ let (Some(owner), Some(style)) = (owner, style) else {
180
+ return hide();
181
+ };
182
+ let (Some(host_root), Some(ring_node)) = FOCUS_ADORNER_STATE.with(|slot| {
183
+ let state = slot.borrow();
184
+ (state.host_root.clone(), state.ring_node.clone())
185
+ }) else {
186
+ return false;
187
+ };
188
+ if owner == NodeHandle::INVALID || host_root.handle() == NodeHandle::INVALID {
189
+ return hide();
190
+ }
191
+ let Some(ring_rect) = resolve_ring_rect(owner) else {
192
+ return hide();
193
+ };
194
+ let Some(visible_rect) = resolve_visible_rect(ring_rect) else {
195
+ return hide();
196
+ };
197
+ let color = current_theme().colors.focus_ring;
198
+ let mut changed = false;
199
+ FOCUS_ADORNER_STATE.with(|slot| {
200
+ let mut state = slot.borrow_mut();
201
+ if !state.attached {
202
+ host_root.child(&ring_node);
203
+ state.attached = true;
204
+ changed = true;
205
+ }
206
+ let relative_ring_x = ring_rect.x - visible_rect.x;
207
+ let relative_ring_y = ring_rect.y - visible_rect.y;
208
+ if visible_rect.x != state.last_host_x
209
+ || visible_rect.y != state.last_host_y
210
+ || visible_rect.width != state.last_host_width
211
+ || visible_rect.height != state.last_host_height
212
+ || relative_ring_x != state.last_ring_x
213
+ || relative_ring_y != state.last_ring_y
214
+ || ring_rect.width != state.last_ring_width
215
+ || ring_rect.height != state.last_ring_height
216
+ {
217
+ host_root.position(visible_rect.x, visible_rect.y);
218
+ host_root.width(visible_rect.width, Unit::Pixel);
219
+ host_root.height(visible_rect.height, Unit::Pixel);
220
+ ring_node.position(relative_ring_x, relative_ring_y);
221
+ ring_node.width(ring_rect.width, Unit::Pixel);
222
+ ring_node.height(ring_rect.height, Unit::Pixel);
223
+ state.last_host_x = visible_rect.x;
224
+ state.last_host_y = visible_rect.y;
225
+ state.last_host_width = visible_rect.width;
226
+ state.last_host_height = visible_rect.height;
227
+ state.last_ring_x = relative_ring_x;
228
+ state.last_ring_y = relative_ring_y;
229
+ state.last_ring_width = ring_rect.width;
230
+ state.last_ring_height = ring_rect.height;
231
+ changed = true;
232
+ }
233
+ if color != state.last_color
234
+ || style.top_left_radius != state.last_top_left_radius
235
+ || style.top_right_radius != state.last_top_right_radius
236
+ || style.bottom_right_radius != state.last_bottom_right_radius
237
+ || style.bottom_left_radius != state.last_bottom_left_radius
238
+ {
239
+ ring_node.corners(
240
+ style.top_left_radius,
241
+ style.top_right_radius,
242
+ style.bottom_right_radius,
243
+ style.bottom_left_radius,
244
+ );
245
+ ring_node.border(STANDARD_FOCUS_RING_WIDTH, color);
246
+ state.last_color = color;
247
+ state.last_top_left_radius = style.top_left_radius;
248
+ state.last_top_right_radius = style.top_right_radius;
249
+ state.last_bottom_right_radius = style.bottom_right_radius;
250
+ state.last_bottom_left_radius = style.bottom_left_radius;
251
+ changed = true;
252
+ }
253
+ });
254
+ changed
255
+ }
256
+
257
+ fn hide() -> bool {
258
+ FOCUS_ADORNER_STATE.with(|slot| {
259
+ let mut state = slot.borrow_mut();
260
+ let (Some(host_root), Some(ring_node)) = (state.host_root.clone(), state.ring_node.clone())
261
+ else {
262
+ state.attached = false;
263
+ state.reset_cached_geometry();
264
+ return false;
265
+ };
266
+ if !state.attached {
267
+ state.attached = false;
268
+ state.reset_cached_geometry();
269
+ return false;
270
+ }
271
+ host_root.remove_child(&ring_node);
272
+ host_root.position(0.0, 0.0);
273
+ host_root.width(0.0, Unit::Pixel);
274
+ host_root.height(0.0, Unit::Pixel);
275
+ state.attached = false;
276
+ state.reset_cached_geometry();
277
+ true
278
+ })
279
+ }
280
+
281
+ fn resolve_ring_rect(owner: NodeHandle) -> Option<FocusAdornerRect> {
282
+ let bounds = ui::get_bounds(owner.raw())?;
283
+ Some(FocusAdornerRect {
284
+ x: bounds[0] - STANDARD_FOCUS_RING_OUTSET,
285
+ y: bounds[1] - STANDARD_FOCUS_RING_OUTSET,
286
+ width: bounds[2] + (STANDARD_FOCUS_RING_OUTSET * 2.0),
287
+ height: bounds[3] + (STANDARD_FOCUS_RING_OUTSET * 2.0),
288
+ })
289
+ }
290
+
291
+ fn resolve_visible_rect(ring_rect: FocusAdornerRect) -> Option<FocusAdornerRect> {
292
+ let min_x = ring_rect.x.max(0.0);
293
+ let min_y = ring_rect.y.max(0.0);
294
+ let max_x = (ring_rect.x + ring_rect.width).min(ui::get_viewport_width());
295
+ let max_y = (ring_rect.y + ring_rect.height).min(ui::get_viewport_height());
296
+ let width = max_x - min_x;
297
+ let height = max_y - min_y;
298
+ if width <= 0.0 || height <= 0.0 {
299
+ return None;
300
+ }
301
+ Some(FocusAdornerRect {
302
+ x: min_x,
303
+ y: min_y,
304
+ width,
305
+ height,
306
+ })
307
+ }
@@ -0,0 +1,121 @@
1
+ use crate::ffi::{KeyEventType, KeyModifier, PointerEventType};
2
+ use crate::signal::{Callback, Signal, SubscriptionGuard};
3
+ use std::cell::RefCell;
4
+ use std::rc::Rc;
5
+
6
+ struct FocusVisibilityState {
7
+ signal: Signal<bool>,
8
+ }
9
+
10
+ thread_local! {
11
+ static FOCUS_VISIBILITY_STATE: RefCell<FocusVisibilityState> = RefCell::new(FocusVisibilityState {
12
+ signal: Signal::new(true),
13
+ });
14
+ }
15
+
16
+ fn current() -> bool {
17
+ FOCUS_VISIBILITY_STATE.with(|slot| slot.borrow().signal.get())
18
+ }
19
+
20
+ fn set(next: bool) {
21
+ let callbacks = FOCUS_VISIBILITY_STATE.with(|slot| slot.borrow_mut().signal.set(next));
22
+ if let Some(callbacks) = callbacks {
23
+ for callback in callbacks {
24
+ callback();
25
+ }
26
+ }
27
+ }
28
+
29
+ fn is_modifier_key(key: &str) -> bool {
30
+ matches!(key, "Shift" | "Control" | "Alt" | "Meta")
31
+ }
32
+
33
+ fn has_non_shift_modifier(modifiers: u32) -> bool {
34
+ (modifiers
35
+ & ((KeyModifier::Ctrl as u32) | (KeyModifier::Alt as u32) | (KeyModifier::Meta as u32)))
36
+ != 0
37
+ }
38
+
39
+ fn is_caret_navigation_key(key: &str) -> bool {
40
+ matches!(
41
+ key,
42
+ "ArrowLeft"
43
+ | "ArrowRight"
44
+ | "ArrowUp"
45
+ | "ArrowDown"
46
+ | "Home"
47
+ | "End"
48
+ | "PageUp"
49
+ | "PageDown"
50
+ )
51
+ }
52
+
53
+ pub(crate) fn keyboard_focus_visible() -> bool {
54
+ current()
55
+ }
56
+
57
+ pub(crate) fn subscribe(handler: impl Fn(bool) + 'static) -> SubscriptionGuard {
58
+ handler(current());
59
+ FOCUS_VISIBILITY_STATE.with(|slot| {
60
+ let callback: Callback = Rc::new(move || handler(current()));
61
+ slot.borrow_mut().signal.subscribe(callback)
62
+ })
63
+ }
64
+
65
+ pub(crate) fn show_keyboard_focus_for_pointer_event(event_type: PointerEventType) {
66
+ if event_type == PointerEventType::Down {
67
+ set(false);
68
+ }
69
+ }
70
+
71
+ pub fn show_keyboard_focus_for_key_event(event_type: KeyEventType, key: &str, modifiers: u32) {
72
+ if event_type != KeyEventType::Down {
73
+ return;
74
+ }
75
+ if is_modifier_key(key) {
76
+ return;
77
+ }
78
+ if has_non_shift_modifier(modifiers) {
79
+ return;
80
+ }
81
+ if is_caret_navigation_key(key) {
82
+ return;
83
+ }
84
+ if key == "Escape" {
85
+ set(false);
86
+ return;
87
+ }
88
+ set(true);
89
+ }
90
+
91
+ pub(crate) fn reset_keyboard_focus_visibility() {
92
+ set(true);
93
+ }
94
+
95
+ #[cfg(test)]
96
+ mod tests {
97
+ use super::*;
98
+
99
+ #[test]
100
+ fn pointer_down_hides_keyboard_focus_visibility() {
101
+ reset_keyboard_focus_visibility();
102
+ show_keyboard_focus_for_pointer_event(PointerEventType::Down);
103
+ assert!(!keyboard_focus_visible());
104
+ }
105
+
106
+ #[test]
107
+ fn regular_key_shows_keyboard_focus_visibility() {
108
+ reset_keyboard_focus_visibility();
109
+ show_keyboard_focus_for_pointer_event(PointerEventType::Down);
110
+ show_keyboard_focus_for_key_event(KeyEventType::Down, "Tab", 0);
111
+ assert!(keyboard_focus_visible());
112
+ }
113
+
114
+ #[test]
115
+ fn modifier_key_does_not_show_keyboard_focus_visibility() {
116
+ reset_keyboard_focus_visibility();
117
+ show_keyboard_focus_for_pointer_event(PointerEventType::Down);
118
+ show_keyboard_focus_for_key_event(KeyEventType::Down, "Shift", 0);
119
+ assert!(!keyboard_focus_visible());
120
+ }
121
+ }
@@ -0,0 +1,159 @@
1
+ use crate::bindings::ui;
2
+ use std::cell::{Cell, RefCell};
3
+
4
+ type LoadedCallback = Box<dyn FnOnce(LoadedEventArgs)>;
5
+
6
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7
+ pub struct LoadedEventArgs;
8
+
9
+ impl LoadedEventArgs {
10
+ pub const EMPTY: Self = Self;
11
+ }
12
+
13
+ thread_local! {
14
+ static NEEDS_COMMIT: Cell<bool> = const { Cell::new(false) };
15
+ static FLUSH_SCHEDULED: Cell<bool> = const { Cell::new(false) };
16
+ static DID_FIRST_COMMIT: Cell<bool> = const { Cell::new(false) };
17
+ static LOADED_CALLBACKS: RefCell<Vec<LoadedCallback>> = const { RefCell::new(Vec::new()) };
18
+ }
19
+
20
+ pub fn mark_needs_commit() {
21
+ NEEDS_COMMIT.with(|needs_commit| needs_commit.set(true));
22
+ let already_scheduled = FLUSH_SCHEDULED.with(|scheduled| {
23
+ let already_scheduled = scheduled.get();
24
+ if !already_scheduled {
25
+ scheduled.set(true);
26
+ }
27
+ already_scheduled
28
+ });
29
+ if !already_scheduled {
30
+ ui::request_render();
31
+ }
32
+ }
33
+
34
+ pub fn on_loaded(callback: impl FnOnce(LoadedEventArgs) + 'static) {
35
+ if DID_FIRST_COMMIT.with(Cell::get) {
36
+ callback(LoadedEventArgs::EMPTY);
37
+ return;
38
+ }
39
+ LOADED_CALLBACKS.with(|callbacks| callbacks.borrow_mut().push(Box::new(callback)));
40
+ }
41
+
42
+ pub(crate) fn fire_loaded_callbacks() {
43
+ if DID_FIRST_COMMIT.with(Cell::get) {
44
+ return;
45
+ }
46
+ DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(true));
47
+ let callbacks = LOADED_CALLBACKS.with(|callbacks| std::mem::take(&mut *callbacks.borrow_mut()));
48
+ for callback in callbacks {
49
+ callback(LoadedEventArgs::EMPTY);
50
+ }
51
+ }
52
+
53
+ pub(crate) fn flush_commit() -> bool {
54
+ FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
55
+ let needs_commit = NEEDS_COMMIT.with(|slot| {
56
+ let needs_commit = slot.get();
57
+ if needs_commit {
58
+ slot.set(false);
59
+ }
60
+ needs_commit
61
+ });
62
+ if !needs_commit {
63
+ return false;
64
+ }
65
+ ui::commit_frame();
66
+ true
67
+ }
68
+
69
+ pub(crate) fn reset_commit_state() {
70
+ NEEDS_COMMIT.with(|needs_commit| needs_commit.set(false));
71
+ FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
72
+ DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(false));
73
+ LOADED_CALLBACKS.with(|callbacks| callbacks.borrow_mut().clear());
74
+ }
75
+
76
+ pub(crate) fn reset_commit_state_preserving_loaded_callbacks() {
77
+ NEEDS_COMMIT.with(|needs_commit| needs_commit.set(false));
78
+ FLUSH_SCHEDULED.with(|scheduled| scheduled.set(false));
79
+ DID_FIRST_COMMIT.with(|did_first_commit| did_first_commit.set(false));
80
+ }
81
+
82
+ #[cfg(test)]
83
+ mod tests {
84
+ use super::*;
85
+ use crate::ffi::{self, Call};
86
+ use std::cell::Cell;
87
+ use std::rc::Rc;
88
+
89
+ #[test]
90
+ fn on_loaded_fires_once_and_late_registration_fires_immediately() {
91
+ reset_commit_state();
92
+ let count = Rc::new(Cell::new(0));
93
+ on_loaded({
94
+ let count = count.clone();
95
+ move |_| count.set(count.get() + 1)
96
+ });
97
+
98
+ fire_loaded_callbacks();
99
+ fire_loaded_callbacks();
100
+ assert_eq!(count.get(), 1);
101
+
102
+ on_loaded({
103
+ let count = count.clone();
104
+ move |_| count.set(count.get() + 1)
105
+ });
106
+ assert_eq!(count.get(), 2);
107
+ }
108
+
109
+ #[test]
110
+ fn mark_needs_commit_coalesces_render_requests_until_flush() {
111
+ reset_commit_state();
112
+ ffi::test::reset();
113
+
114
+ mark_needs_commit();
115
+ mark_needs_commit();
116
+ let calls = ffi::test::take_calls();
117
+ assert_eq!(
118
+ calls
119
+ .iter()
120
+ .filter(|call| matches!(call, Call::RequestRender))
121
+ .count(),
122
+ 1
123
+ );
124
+
125
+ assert!(flush_commit());
126
+ let _ = ffi::test::take_calls();
127
+ mark_needs_commit();
128
+ let calls = ffi::test::take_calls();
129
+ assert_eq!(
130
+ calls
131
+ .iter()
132
+ .filter(|call| matches!(call, Call::RequestRender))
133
+ .count(),
134
+ 1
135
+ );
136
+ }
137
+
138
+ #[test]
139
+ fn flush_commit_noops_without_pending_work() {
140
+ reset_commit_state();
141
+ ffi::test::reset();
142
+
143
+ assert!(!flush_commit());
144
+ assert!(ffi::test::take_calls()
145
+ .iter()
146
+ .all(|call| !matches!(call, Call::CommitFrame)));
147
+
148
+ mark_needs_commit();
149
+ assert!(flush_commit());
150
+ assert!(ffi::test::take_calls()
151
+ .iter()
152
+ .any(|call| matches!(call, Call::CommitFrame)));
153
+
154
+ assert!(!flush_commit());
155
+ assert!(ffi::test::take_calls()
156
+ .iter()
157
+ .all(|call| !matches!(call, Call::CommitFrame)));
158
+ }
159
+ }
@@ -0,0 +1,64 @@
1
+ use crate::signal::{Callback, Signal, SubscriptionGuard};
2
+ use std::cell::RefCell;
3
+ use std::rc::Rc;
4
+
5
+ #[derive(Clone, Copy)]
6
+ pub struct FrameTimeSignalHandle;
7
+
8
+ thread_local! {
9
+ static FRAME_TIME_SIGNAL: RefCell<Signal<f64>> = RefCell::new(Signal::new(0.0));
10
+ }
11
+
12
+ impl FrameTimeSignalHandle {
13
+ pub fn value(&self) -> f64 {
14
+ FRAME_TIME_SIGNAL.with(|slot| slot.borrow().get())
15
+ }
16
+
17
+ pub fn subscribe(&self, handler: impl Fn(f64) + 'static) -> SubscriptionGuard {
18
+ handler(self.value());
19
+ FRAME_TIME_SIGNAL.with(|slot| {
20
+ let callback: Callback = Rc::new(move || handler(frame_time_signal().value()));
21
+ slot.borrow_mut().subscribe(callback)
22
+ })
23
+ }
24
+ }
25
+
26
+ pub fn frame_time_signal() -> FrameTimeSignalHandle {
27
+ FrameTimeSignalHandle
28
+ }
29
+
30
+ pub(crate) fn set_frame_time(timestamp_ms: f64) {
31
+ FRAME_TIME_SIGNAL.with(|slot| {
32
+ let callbacks = slot.borrow_mut().set(timestamp_ms);
33
+ if let Some(callbacks) = callbacks {
34
+ for callback in callbacks {
35
+ callback();
36
+ }
37
+ }
38
+ });
39
+ }
40
+
41
+ #[cfg(test)]
42
+ mod tests {
43
+ use super::{frame_time_signal, set_frame_time};
44
+ use std::cell::Cell;
45
+ use std::rc::Rc;
46
+
47
+ #[test]
48
+ fn updates_frame_time_signal_and_notifies_subscribers() {
49
+ let count = Rc::new(Cell::new(0));
50
+ let seen = Rc::new(Cell::new(0.0));
51
+ let count_for_handler = count.clone();
52
+ let seen_for_handler = seen.clone();
53
+ let _guard = frame_time_signal().subscribe(move |value| {
54
+ count_for_handler.set(count_for_handler.get() + 1);
55
+ seen_for_handler.set(value);
56
+ });
57
+
58
+ set_frame_time(1234.5);
59
+
60
+ assert_eq!(frame_time_signal().value(), 1234.5);
61
+ assert_eq!(seen.get(), 1234.5);
62
+ assert!(count.get() >= 2);
63
+ }
64
+ }