@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
package/src/bitmap.rs ADDED
@@ -0,0 +1,317 @@
1
+ use crate::assets;
2
+ use crate::drawing::DrawContext;
3
+ use crate::ffi;
4
+ use crate::frame_scheduler::on_loaded;
5
+ use crate::logger::error;
6
+ use crate::node::Node;
7
+ use crate::text::TextLayout;
8
+ use std::cell::RefCell;
9
+ use std::rc::Rc;
10
+
11
+ const MAX_DIRTY_RECTS: usize = 16;
12
+
13
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14
+ pub struct BitmapTextReadyEventArgs;
15
+
16
+ impl BitmapTextReadyEventArgs {
17
+ pub const EMPTY: Self = Self;
18
+ }
19
+
20
+ struct BitmapState {
21
+ width: u32,
22
+ height: u32,
23
+ texture_id: u32,
24
+ pixel_bytes: Vec<u8>,
25
+ offscreen_id: u32,
26
+ canvas_used: bool,
27
+ draw_context: Option<DrawContext>,
28
+ disposed: bool,
29
+ dirty_rects: Vec<(u32, u32, u32, u32)>,
30
+ }
31
+
32
+ #[derive(Clone)]
33
+ pub struct Bitmap {
34
+ inner: Rc<RefCell<BitmapState>>,
35
+ }
36
+
37
+ impl Bitmap {
38
+ pub fn new(width: u32, height: u32) -> Self {
39
+ assert!(
40
+ width > 0 && height > 0,
41
+ "Bitmap width and height must be greater than zero."
42
+ );
43
+ let byte_len = (width as usize)
44
+ .checked_mul(height as usize)
45
+ .and_then(|value| value.checked_mul(4))
46
+ .expect("Bitmap byte length overflow.");
47
+ let texture_id = assets::allocate_dynamic_texture_id();
48
+ let offscreen_id = unsafe { ffi::fui_canvas_create_offscreen(width, height) };
49
+ Self {
50
+ inner: Rc::new(RefCell::new(BitmapState {
51
+ width,
52
+ height,
53
+ texture_id,
54
+ pixel_bytes: vec![0; byte_len],
55
+ offscreen_id,
56
+ canvas_used: false,
57
+ draw_context: None,
58
+ disposed: false,
59
+ dirty_rects: Vec::new(),
60
+ })),
61
+ }
62
+ }
63
+
64
+ pub fn width(&self) -> u32 {
65
+ self.inner.borrow().width
66
+ }
67
+
68
+ pub fn height(&self) -> u32 {
69
+ self.inner.borrow().height
70
+ }
71
+
72
+ pub fn texture_id(&self) -> u32 {
73
+ self.inner.borrow().texture_id
74
+ }
75
+
76
+ pub fn pixels(&self) -> std::cell::RefMut<'_, Vec<u8>> {
77
+ std::cell::RefMut::map(self.inner.borrow_mut(), |state| {
78
+ assert!(!state.disposed, "Bitmap.pixels() called after dispose.");
79
+ &mut state.pixel_bytes
80
+ })
81
+ }
82
+
83
+ pub fn pixel_ptr(&self) -> usize {
84
+ let state = self.inner.borrow();
85
+ if state.pixel_bytes.is_empty() {
86
+ 0
87
+ } else {
88
+ state.pixel_bytes.as_ptr() as usize
89
+ }
90
+ }
91
+
92
+ pub fn canvas(&self) -> DrawContext {
93
+ let mut state = self.inner.borrow_mut();
94
+ assert!(!state.disposed, "Bitmap.canvas() called after dispose.");
95
+ state.canvas_used = true;
96
+ if let Some(context) = &state.draw_context {
97
+ return context.clone();
98
+ }
99
+ let ptr = unsafe { ffi::fui_canvas_get_offscreen_ptr(state.offscreen_id) };
100
+ let context = DrawContext::new(ptr);
101
+ state.draw_context = Some(context.clone());
102
+ context
103
+ }
104
+
105
+ pub fn render<T: Node>(&self, node: &T, x: f32, y: f32, scale: f32) {
106
+ let state = self.inner.borrow();
107
+ assert!(!state.disposed, "Bitmap.render() called after dispose.");
108
+ let handle = node.handle().raw();
109
+ if handle == 0 {
110
+ return;
111
+ }
112
+ unsafe {
113
+ ffi::fui_render_node_to_rgba(
114
+ handle,
115
+ state.width,
116
+ state.height,
117
+ state.pixel_bytes.as_ptr() as usize,
118
+ state.pixel_bytes.len() as u32,
119
+ scale,
120
+ x,
121
+ y,
122
+ )
123
+ };
124
+ }
125
+
126
+ pub fn render_text_layout(&self, layout: &TextLayout, x: f32, y: f32, scale: f32) {
127
+ if !layout.is_ready() {
128
+ error(
129
+ "TextLayout",
130
+ "Bitmap.render_text_layout() called before the TextLayout was ready; register on_ready and render after the callback.",
131
+ );
132
+ return;
133
+ }
134
+ let node = layout.draw_node();
135
+ self.render(&node, x, y, scale);
136
+ }
137
+
138
+ pub fn prepare_text<T: Node>(node: &T) {
139
+ crate::bindings::ui::prepare_node(node.handle().raw());
140
+ }
141
+
142
+ pub fn on_text_ready<T: Node + Clone + 'static>(
143
+ &self,
144
+ node: &T,
145
+ callback: impl FnOnce(BitmapTextReadyEventArgs) + 'static,
146
+ ) -> &Self {
147
+ let node = node.clone();
148
+ on_loaded(move |_| {
149
+ Self::prepare_text(&node);
150
+ callback(BitmapTextReadyEventArgs::EMPTY);
151
+ });
152
+ self
153
+ }
154
+
155
+ pub fn dirty_rect(&self, x: u32, y: u32, w: u32, h: u32) -> &Self {
156
+ let mut state = self.inner.borrow_mut();
157
+ if w == 0 || h == 0 || x >= state.width || y >= state.height {
158
+ return self;
159
+ }
160
+ let cw = (x + w).min(state.width) - x;
161
+ let ch = (y + h).min(state.height) - y;
162
+ if state.dirty_rects.len() < MAX_DIRTY_RECTS {
163
+ state.dirty_rects.push((x, y, cw, ch));
164
+ }
165
+ self
166
+ }
167
+
168
+ pub fn clear_dirty_rects(&self) -> &Self {
169
+ self.inner.borrow_mut().dirty_rects.clear();
170
+ self
171
+ }
172
+
173
+ pub fn has_dirty_rects(&self) -> bool {
174
+ !self.inner.borrow().dirty_rects.is_empty()
175
+ }
176
+
177
+ pub fn commit(&self) -> u32 {
178
+ let mut state = self.inner.borrow_mut();
179
+ assert!(!state.disposed, "Bitmap.commit() called after dispose.");
180
+ if state.canvas_used {
181
+ if let Some(context) = &state.draw_context {
182
+ context.flush();
183
+ }
184
+ unsafe {
185
+ ffi::fui_canvas_read_offscreen_pixels(
186
+ state.offscreen_id,
187
+ state.pixel_bytes.as_mut_ptr() as usize,
188
+ state.width,
189
+ state.height,
190
+ )
191
+ };
192
+ }
193
+ if state.dirty_rects.is_empty() {
194
+ unsafe {
195
+ ffi::fui_bitmap_commit(
196
+ state.texture_id,
197
+ state.pixel_bytes.as_ptr() as usize,
198
+ state.pixel_bytes.len() as u32,
199
+ state.width,
200
+ state.height,
201
+ )
202
+ };
203
+ } else {
204
+ let dirty_rects = std::mem::take(&mut state.dirty_rects);
205
+ for (x, y, w, h) in dirty_rects {
206
+ let mut rect_bytes = vec![0u8; (w as usize) * (h as usize) * 4];
207
+ for row in 0..h as usize {
208
+ let src = (((y as usize + row) * state.width as usize) + x as usize) * 4;
209
+ let dst = row * w as usize * 4;
210
+ let len = w as usize * 4;
211
+ rect_bytes[dst..dst + len].copy_from_slice(&state.pixel_bytes[src..src + len]);
212
+ }
213
+ unsafe {
214
+ ffi::fui_bitmap_commit_dirty(
215
+ state.texture_id,
216
+ rect_bytes.as_ptr() as usize,
217
+ rect_bytes.len() as u32,
218
+ state.width,
219
+ state.height,
220
+ x,
221
+ y,
222
+ w,
223
+ h,
224
+ )
225
+ };
226
+ }
227
+ }
228
+ assets::mark_texture_asset_ready(state.texture_id, state.width as f32, state.height as f32);
229
+ state.texture_id
230
+ }
231
+
232
+ pub fn dispose(&self) {
233
+ let mut state = self.inner.borrow_mut();
234
+ if state.disposed {
235
+ return;
236
+ }
237
+ state.disposed = true;
238
+ unsafe { ffi::fui_canvas_destroy_offscreen(state.offscreen_id) };
239
+ unsafe { ffi::fui_bitmap_release(state.texture_id) };
240
+ state.pixel_bytes.clear();
241
+ state.draw_context = None;
242
+ }
243
+ }
244
+
245
+ impl Drop for Bitmap {
246
+ fn drop(&mut self) {
247
+ if Rc::strong_count(&self.inner) == 1 {
248
+ self.dispose();
249
+ }
250
+ }
251
+ }
252
+
253
+ #[cfg(test)]
254
+ mod tests {
255
+ use super::Bitmap;
256
+ use crate::drawing::Paint;
257
+ use crate::ffi::{self, Call};
258
+
259
+ #[test]
260
+ fn bitmap_canvas_commit_flushes_offscreen_and_marks_asset_ready() {
261
+ ffi::test::reset();
262
+ let bitmap = Bitmap::new(32, 24);
263
+ let canvas = bitmap.canvas();
264
+ canvas.draw_rect(0.0, 0.0, 10.0, 12.0, Paint::fill(0xFF00FFFF));
265
+ bitmap.commit();
266
+
267
+ let calls = ffi::test::take_calls();
268
+ assert!(calls.iter().any(|call| matches!(
269
+ call,
270
+ Call::CanvasCreateOffscreen {
271
+ width: 32,
272
+ height: 24,
273
+ ..
274
+ }
275
+ )));
276
+ assert!(calls
277
+ .iter()
278
+ .any(|call| matches!(call, Call::CanvasDrawBatch { .. })));
279
+ assert!(calls.iter().any(|call| matches!(
280
+ call,
281
+ Call::CanvasReadOffscreenPixels {
282
+ width: 32,
283
+ height: 24,
284
+ ..
285
+ }
286
+ )));
287
+ assert!(calls.iter().any(|call| matches!(
288
+ call,
289
+ Call::BitmapCommit {
290
+ width: 32,
291
+ height: 24,
292
+ ..
293
+ }
294
+ )));
295
+ }
296
+
297
+ #[test]
298
+ fn bitmap_dirty_commit_uses_subrect_upload() {
299
+ ffi::test::reset();
300
+ let bitmap = Bitmap::new(8, 6);
301
+ bitmap.dirty_rect(2, 1, 3, 2).commit();
302
+
303
+ let calls = ffi::test::take_calls();
304
+ assert!(calls.iter().any(|call| matches!(
305
+ call,
306
+ Call::BitmapCommitDirty {
307
+ full_width: 8,
308
+ full_height: 6,
309
+ sub_x: 2,
310
+ sub_y: 1,
311
+ sub_w: 3,
312
+ sub_h: 2,
313
+ ..
314
+ }
315
+ )));
316
+ }
317
+ }
@@ -0,0 +1,242 @@
1
+ use crate::animation;
2
+ use crate::assets;
3
+ use crate::bindings::ui;
4
+ use crate::context_menu_manager;
5
+ use crate::event;
6
+ use crate::frame_signal;
7
+ use crate::viewport;
8
+ use crate::Application;
9
+ use std::cell::{Cell, RefCell};
10
+
11
+ #[derive(Clone, Debug, Default, PartialEq)]
12
+ pub struct ScrollEvent {
13
+ pub handle: u64,
14
+ pub offset_x: f32,
15
+ pub offset_y: f32,
16
+ pub content_width: f32,
17
+ pub content_height: f32,
18
+ pub viewport_width: f32,
19
+ pub viewport_height: f32,
20
+ }
21
+
22
+ #[derive(Clone, Debug, Default, PartialEq)]
23
+ pub struct ContextMenuRequest {
24
+ pub handle: u64,
25
+ pub x: f32,
26
+ pub y: f32,
27
+ }
28
+
29
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
30
+ pub struct AssetFailure {
31
+ pub id: u32,
32
+ pub error: String,
33
+ }
34
+
35
+ #[derive(Clone, Debug, Default, PartialEq)]
36
+ pub struct AssetReady {
37
+ pub id: u32,
38
+ pub width: f32,
39
+ pub height: f32,
40
+ }
41
+
42
+ thread_local! {
43
+ static CURRENT_ROUTE: RefCell<String> = const { RefCell::new(String::new()) };
44
+ static LAST_SCROLL: RefCell<Option<ScrollEvent>> = const { RefCell::new(None) };
45
+ static LAST_CONTEXT_MENU: RefCell<Option<ContextMenuRequest>> = const { RefCell::new(None) };
46
+ static CONTEXT_MENU_VISIBLE: Cell<bool> = const { Cell::new(false) };
47
+ static LAST_FONT_LOADED: Cell<Option<u32>> = const { Cell::new(None) };
48
+ static LAST_SVG_LOADED: RefCell<Option<AssetReady>> = const { RefCell::new(None) };
49
+ static LAST_SVG_FAILED: RefCell<Option<AssetFailure>> = const { RefCell::new(None) };
50
+ static LAST_TEXTURE_LOADED: RefCell<Option<AssetReady>> = const { RefCell::new(None) };
51
+ static LAST_TEXTURE_FAILED: RefCell<Option<AssetFailure>> = const { RefCell::new(None) };
52
+ static PERSIST_CAPTURE_COUNT: Cell<u32> = const { Cell::new(0) };
53
+ static PERSIST_RESTORE_COUNT: Cell<u32> = const { Cell::new(0) };
54
+ }
55
+
56
+ fn read_utf8(ptr: *const u8, len: u32) -> String {
57
+ if ptr.is_null() || len == 0 {
58
+ return String::new();
59
+ }
60
+ let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
61
+ String::from_utf8_lossy(bytes).into_owned()
62
+ }
63
+
64
+ pub fn current_route() -> String {
65
+ CURRENT_ROUTE.with(|route| route.borrow().clone())
66
+ }
67
+
68
+ pub fn last_scroll_event() -> Option<ScrollEvent> {
69
+ LAST_SCROLL.with(|event| event.borrow().clone())
70
+ }
71
+
72
+ pub fn last_context_menu_request() -> Option<ContextMenuRequest> {
73
+ LAST_CONTEXT_MENU.with(|event| event.borrow().clone())
74
+ }
75
+
76
+ pub fn is_context_menu_visible() -> bool {
77
+ CONTEXT_MENU_VISIBLE.with(Cell::get)
78
+ }
79
+
80
+ pub fn last_font_loaded() -> Option<u32> {
81
+ LAST_FONT_LOADED.with(Cell::get)
82
+ }
83
+
84
+ pub fn last_svg_loaded() -> Option<AssetReady> {
85
+ LAST_SVG_LOADED.with(|event| event.borrow().clone())
86
+ }
87
+
88
+ pub fn last_svg_failed() -> Option<AssetFailure> {
89
+ LAST_SVG_FAILED.with(|event| event.borrow().clone())
90
+ }
91
+
92
+ pub fn last_texture_loaded() -> Option<AssetReady> {
93
+ LAST_TEXTURE_LOADED.with(|event| event.borrow().clone())
94
+ }
95
+
96
+ pub fn last_texture_failed() -> Option<AssetFailure> {
97
+ LAST_TEXTURE_FAILED.with(|event| event.borrow().clone())
98
+ }
99
+
100
+ pub fn persisted_capture_count() -> u32 {
101
+ PERSIST_CAPTURE_COUNT.with(Cell::get)
102
+ }
103
+
104
+ pub fn persisted_restore_count() -> u32 {
105
+ PERSIST_RESTORE_COUNT.with(Cell::get)
106
+ }
107
+
108
+ #[no_mangle]
109
+ pub extern "C" fn __fui_on_viewport_changed(width: f32, height: f32) {
110
+ ui::resize_window(width, height);
111
+ viewport::set_viewport_size(width, height);
112
+ }
113
+
114
+ #[no_mangle]
115
+ pub extern "C" fn __fui_on_frame(timestamp_ms: f64) {
116
+ frame_signal::set_frame_time(timestamp_ms);
117
+ animation::tick_animations(timestamp_ms);
118
+ }
119
+
120
+ #[no_mangle]
121
+ pub extern "C" fn __fui_on_route_changed(route_ptr: *const u8, route_len: u32) {
122
+ let route = read_utf8(route_ptr, route_len);
123
+ CURRENT_ROUTE.with(|slot| slot.replace(route));
124
+ }
125
+
126
+ #[no_mangle]
127
+ pub extern "C" fn __fui_on_scroll(
128
+ handle: u64,
129
+ offset_x: f32,
130
+ offset_y: f32,
131
+ content_width: f32,
132
+ content_height: f32,
133
+ viewport_width: f32,
134
+ viewport_height: f32,
135
+ ) {
136
+ LAST_SCROLL.with(|slot| {
137
+ slot.replace(Some(ScrollEvent {
138
+ handle,
139
+ offset_x,
140
+ offset_y,
141
+ content_width,
142
+ content_height,
143
+ viewport_width,
144
+ viewport_height,
145
+ }));
146
+ });
147
+ event::dispatch_scroll(
148
+ crate::node::NodeHandle::from_raw(handle),
149
+ offset_x,
150
+ offset_y,
151
+ content_width,
152
+ content_height,
153
+ viewport_width,
154
+ viewport_height,
155
+ );
156
+ }
157
+
158
+ #[no_mangle]
159
+ pub extern "C" fn __fui_can_show_context_menu(handle: u64) -> bool {
160
+ context_menu_manager::can_show_for_handle(handle)
161
+ }
162
+
163
+ #[no_mangle]
164
+ pub extern "C" fn __fui_on_context_menu(handle: u64, x: f32, y: f32) {
165
+ LAST_CONTEXT_MENU.with(|slot| slot.replace(Some(ContextMenuRequest { handle, x, y })));
166
+ let shown = context_menu_manager::show_for_current_selection(handle, x, y);
167
+ CONTEXT_MENU_VISIBLE.with(|visible| visible.set(shown));
168
+ }
169
+
170
+ #[no_mangle]
171
+ pub extern "C" fn __fui_hide_active_context_menu() {
172
+ context_menu_manager::hide_active_menu();
173
+ CONTEXT_MENU_VISIBLE.with(|visible| visible.set(false));
174
+ }
175
+
176
+ #[no_mangle]
177
+ pub extern "C" fn __fui_on_font_loaded(font_id: u32) {
178
+ LAST_FONT_LOADED.with(|slot| slot.set(Some(font_id)));
179
+ assets::on_font_loaded(font_id);
180
+ crate::typography::notify_font_loaded(font_id);
181
+ crate::text::notify_font_loaded(font_id);
182
+ }
183
+
184
+ #[no_mangle]
185
+ pub extern "C" fn __fui_on_svg_loaded(svg_id: u32, width: f32, height: f32) {
186
+ LAST_SVG_LOADED.with(|slot| {
187
+ slot.replace(Some(AssetReady {
188
+ id: svg_id,
189
+ width,
190
+ height,
191
+ }))
192
+ });
193
+ assets::on_svg_loaded(svg_id, width, height);
194
+ }
195
+
196
+ #[no_mangle]
197
+ pub extern "C" fn __fui_on_svg_failed(svg_id: u32, error_ptr: *const u8, error_len: u32) {
198
+ let error = read_utf8(error_ptr, error_len);
199
+ LAST_SVG_FAILED.with(|slot| {
200
+ slot.replace(Some(AssetFailure {
201
+ id: svg_id,
202
+ error: error.clone(),
203
+ }))
204
+ });
205
+ assets::on_svg_failed(svg_id, error);
206
+ }
207
+
208
+ #[no_mangle]
209
+ pub extern "C" fn __fui_on_texture_loaded(texture_id: u32, width: f32, height: f32) {
210
+ LAST_TEXTURE_LOADED.with(|slot| {
211
+ slot.replace(Some(AssetReady {
212
+ id: texture_id,
213
+ width,
214
+ height,
215
+ }))
216
+ });
217
+ assets::on_texture_loaded(texture_id, width, height);
218
+ }
219
+
220
+ #[no_mangle]
221
+ pub extern "C" fn __fui_on_texture_failed(texture_id: u32, error_ptr: *const u8, error_len: u32) {
222
+ let error = read_utf8(error_ptr, error_len);
223
+ LAST_TEXTURE_FAILED.with(|slot| {
224
+ slot.replace(Some(AssetFailure {
225
+ id: texture_id,
226
+ error: error.clone(),
227
+ }))
228
+ });
229
+ assets::on_texture_failed(texture_id, error);
230
+ }
231
+
232
+ #[no_mangle]
233
+ pub extern "C" fn __fui_capture_persisted_ui_state() {
234
+ PERSIST_CAPTURE_COUNT.with(|count| count.set(count.get() + 1));
235
+ Application::capture_persisted_ui_state();
236
+ }
237
+
238
+ #[no_mangle]
239
+ pub extern "C" fn __fui_restore_persisted_ui_state() {
240
+ PERSIST_RESTORE_COUNT.with(|count| count.set(count.get() + 1));
241
+ Application::restore_persisted_ui_state();
242
+ }