@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/assets.rs ADDED
@@ -0,0 +1,572 @@
1
+ use crate::ffi;
2
+ use crate::logger::warn;
3
+ use crate::signal::{Callback, Signal, SubscriptionGuard};
4
+ use std::cell::{Cell, RefCell};
5
+ use std::collections::{HashMap, HashSet};
6
+ use std::rc::Rc;
7
+ use std::thread::LocalKey;
8
+
9
+ const FIRST_DYNAMIC_SVG_ID: u32 = 0x1000_0000;
10
+ const FIRST_DYNAMIC_TEXTURE_ID: u32 = 0x2000_0000;
11
+ const MAX_DYNAMIC_ASSET_ID: u32 = 0xffff_ffff;
12
+
13
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
14
+ pub enum AssetLoadState {
15
+ Idle = 0,
16
+ Loading = 1,
17
+ Ready = 2,
18
+ Failed = 3,
19
+ }
20
+
21
+ #[derive(Clone)]
22
+ pub struct AssetStateSignal {
23
+ inner: Rc<RefCell<Signal<AssetLoadState>>>,
24
+ }
25
+
26
+ impl AssetStateSignal {
27
+ fn new(initial: AssetLoadState) -> Self {
28
+ Self {
29
+ inner: Rc::new(RefCell::new(Signal::new(initial))),
30
+ }
31
+ }
32
+
33
+ pub fn get(&self) -> AssetLoadState {
34
+ self.inner.borrow().get()
35
+ }
36
+
37
+ pub fn subscribe(&self, callback: Callback) -> SubscriptionGuard {
38
+ self.inner.borrow_mut().subscribe(callback)
39
+ }
40
+
41
+ fn set(&self, next: AssetLoadState) {
42
+ let callbacks = self.inner.borrow_mut().set(next);
43
+ if let Some(callbacks) = callbacks {
44
+ for callback in callbacks {
45
+ callback();
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ #[derive(Clone)]
52
+ struct AssetRecord {
53
+ state: AssetStateSignal,
54
+ error: String,
55
+ width: f32,
56
+ height: f32,
57
+ url: String,
58
+ }
59
+
60
+ impl Default for AssetRecord {
61
+ fn default() -> Self {
62
+ Self {
63
+ state: AssetStateSignal::new(AssetLoadState::Idle),
64
+ error: String::new(),
65
+ width: 0.0,
66
+ height: 0.0,
67
+ url: String::new(),
68
+ }
69
+ }
70
+ }
71
+
72
+ thread_local! {
73
+ static SVG_ASSETS: RefCell<HashMap<u32, Rc<RefCell<AssetRecord>>>> = RefCell::new(HashMap::new());
74
+ static TEXTURE_ASSETS: RefCell<HashMap<u32, Rc<RefCell<AssetRecord>>>> = RefCell::new(HashMap::new());
75
+ static LOADED_FONT_IDS: RefCell<HashSet<u32>> = RefCell::new(default_loaded_font_ids());
76
+ static SVG_IDS_BY_URL: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
77
+ static TEXTURE_IDS_BY_URL: RefCell<HashMap<String, u32>> = RefCell::new(HashMap::new());
78
+ static SVG_REF_COUNTS: RefCell<HashMap<u32, i32>> = RefCell::new(HashMap::new());
79
+ static TEXTURE_REF_COUNTS: RefCell<HashMap<u32, i32>> = RefCell::new(HashMap::new());
80
+ static PINNED_SVG_IDS: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
81
+ static PINNED_TEXTURE_IDS: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
82
+ static NEXT_DYNAMIC_SVG_ID: Cell<u32> = const { Cell::new(FIRST_DYNAMIC_SVG_ID) };
83
+ static NEXT_DYNAMIC_TEXTURE_ID: Cell<u32> = const { Cell::new(FIRST_DYNAMIC_TEXTURE_ID) };
84
+ }
85
+
86
+ fn default_loaded_font_ids() -> HashSet<u32> {
87
+ HashSet::from([1, 2, 7])
88
+ }
89
+
90
+ fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
91
+ let bytes = value.as_bytes();
92
+ callback(
93
+ if bytes.is_empty() {
94
+ 0
95
+ } else {
96
+ bytes.as_ptr() as usize
97
+ },
98
+ bytes.len() as u32,
99
+ );
100
+ }
101
+
102
+ fn next_dynamic_svg_id() -> u32 {
103
+ NEXT_DYNAMIC_SVG_ID.with(|slot| {
104
+ let next = slot.get();
105
+ assert!(
106
+ next != MAX_DYNAMIC_ASSET_ID,
107
+ "Dynamic SVG asset id space exhausted."
108
+ );
109
+ slot.set(next + 1);
110
+ next
111
+ })
112
+ }
113
+
114
+ fn next_dynamic_texture_id() -> u32 {
115
+ NEXT_DYNAMIC_TEXTURE_ID.with(|slot| {
116
+ let next = slot.get();
117
+ assert!(
118
+ next != MAX_DYNAMIC_ASSET_ID,
119
+ "Dynamic texture asset id space exhausted."
120
+ );
121
+ slot.set(next + 1);
122
+ next
123
+ })
124
+ }
125
+
126
+ fn get_svg_record(svg_id: u32) -> Rc<RefCell<AssetRecord>> {
127
+ SVG_ASSETS.with(|records| {
128
+ let mut records = records.borrow_mut();
129
+ records
130
+ .entry(svg_id)
131
+ .or_insert_with(|| Rc::new(RefCell::new(AssetRecord::default())))
132
+ .clone()
133
+ })
134
+ }
135
+
136
+ fn get_texture_record(texture_id: u32) -> Rc<RefCell<AssetRecord>> {
137
+ TEXTURE_ASSETS.with(|records| {
138
+ let mut records = records.borrow_mut();
139
+ records
140
+ .entry(texture_id)
141
+ .or_insert_with(|| Rc::new(RefCell::new(AssetRecord::default())))
142
+ .clone()
143
+ })
144
+ }
145
+
146
+ fn begin_load(record: &Rc<RefCell<AssetRecord>>) {
147
+ let state = {
148
+ let mut record = record.borrow_mut();
149
+ record.error.clear();
150
+ record.width = 0.0;
151
+ record.height = 0.0;
152
+ record.state.clone()
153
+ };
154
+ state.set(AssetLoadState::Loading);
155
+ }
156
+
157
+ fn mark_loaded(record: &Rc<RefCell<AssetRecord>>, width: f32, height: f32) {
158
+ let state = {
159
+ let mut record = record.borrow_mut();
160
+ record.error.clear();
161
+ record.width = width;
162
+ record.height = height;
163
+ record.state.clone()
164
+ };
165
+ state.set(AssetLoadState::Ready);
166
+ }
167
+
168
+ fn mark_failed(record: &Rc<RefCell<AssetRecord>>, error: String) {
169
+ let state = {
170
+ let mut record = record.borrow_mut();
171
+ record.error = error;
172
+ record.width = 0.0;
173
+ record.height = 0.0;
174
+ record.state.clone()
175
+ };
176
+ state.set(AssetLoadState::Failed);
177
+ }
178
+
179
+ fn increment_ref_count(ref_counts: &RefCell<HashMap<u32, i32>>, asset_id: u32) {
180
+ let mut ref_counts = ref_counts.borrow_mut();
181
+ let next = ref_counts.get(&asset_id).copied().unwrap_or(0) + 1;
182
+ ref_counts.insert(asset_id, next);
183
+ }
184
+
185
+ fn decrement_ref_count(ref_counts: &RefCell<HashMap<u32, i32>>, asset_id: u32) -> i32 {
186
+ let mut ref_counts = ref_counts.borrow_mut();
187
+ let Some(current) = ref_counts.get(&asset_id).copied() else {
188
+ return -1;
189
+ };
190
+ let next = current - 1;
191
+ if next <= 0 {
192
+ ref_counts.remove(&asset_id);
193
+ 0
194
+ } else {
195
+ ref_counts.insert(asset_id, next);
196
+ next
197
+ }
198
+ }
199
+
200
+ fn remove_url_binding(
201
+ url_to_id: &'static LocalKey<RefCell<HashMap<String, u32>>>,
202
+ url: &str,
203
+ asset_id: u32,
204
+ ) {
205
+ if url.is_empty() {
206
+ return;
207
+ }
208
+ url_to_id.with(|url_to_id| {
209
+ let mut url_to_id = url_to_id.borrow_mut();
210
+ if url_to_id.get(url).copied() == Some(asset_id) {
211
+ url_to_id.remove(url);
212
+ }
213
+ });
214
+ }
215
+
216
+ fn load_svg_internal(svg_id: u32, url: &str, pinned: bool) {
217
+ let record = get_svg_record(svg_id);
218
+ {
219
+ let mut record_mut = record.borrow_mut();
220
+ if !record_mut.url.is_empty() && record_mut.url != url {
221
+ remove_url_binding(&SVG_IDS_BY_URL, &record_mut.url, svg_id);
222
+ }
223
+ record_mut.url = url.to_string();
224
+ }
225
+ SVG_IDS_BY_URL.with(|map| {
226
+ map.borrow_mut().insert(url.to_string(), svg_id);
227
+ });
228
+ PINNED_SVG_IDS.with(|ids| {
229
+ let mut ids = ids.borrow_mut();
230
+ if pinned {
231
+ ids.insert(svg_id);
232
+ } else {
233
+ ids.remove(&svg_id);
234
+ }
235
+ });
236
+ begin_load(&record);
237
+ with_utf8(url, |ptr, len| unsafe {
238
+ ffi::fui_load_svg(svg_id, ptr, len);
239
+ });
240
+ }
241
+
242
+ fn load_texture_internal(texture_id: u32, url: &str, pinned: bool) {
243
+ let record = get_texture_record(texture_id);
244
+ {
245
+ let mut record_mut = record.borrow_mut();
246
+ if !record_mut.url.is_empty() && record_mut.url != url {
247
+ remove_url_binding(&TEXTURE_IDS_BY_URL, &record_mut.url, texture_id);
248
+ }
249
+ record_mut.url = url.to_string();
250
+ }
251
+ TEXTURE_IDS_BY_URL.with(|map| {
252
+ map.borrow_mut().insert(url.to_string(), texture_id);
253
+ });
254
+ PINNED_TEXTURE_IDS.with(|ids| {
255
+ let mut ids = ids.borrow_mut();
256
+ if pinned {
257
+ ids.insert(texture_id);
258
+ } else {
259
+ ids.remove(&texture_id);
260
+ }
261
+ });
262
+ begin_load(&record);
263
+ with_utf8(url, |ptr, len| unsafe {
264
+ ffi::fui_load_texture(texture_id, ptr, len);
265
+ });
266
+ }
267
+
268
+ pub(crate) fn load_font(font_id: u32, url: &str) {
269
+ LOADED_FONT_IDS.with(|ids| {
270
+ ids.borrow_mut().remove(&font_id);
271
+ });
272
+ with_utf8(url, |ptr, len| unsafe {
273
+ ffi::fui_load_font(font_id, ptr, len);
274
+ });
275
+ }
276
+
277
+ pub fn allocate_dynamic_texture_id() -> u32 {
278
+ next_dynamic_texture_id()
279
+ }
280
+
281
+ pub(crate) fn is_font_loaded(font_id: u32) -> bool {
282
+ LOADED_FONT_IDS.with(|ids| ids.borrow().contains(&font_id))
283
+ }
284
+
285
+ pub(crate) fn on_font_loaded(font_id: u32) {
286
+ LOADED_FONT_IDS.with(|ids| {
287
+ ids.borrow_mut().insert(font_id);
288
+ });
289
+ }
290
+
291
+ pub fn load_svg(svg_id: u32, url: &str) {
292
+ load_svg_internal(svg_id, url, true);
293
+ }
294
+
295
+ pub fn load_texture(texture_id: u32, url: &str) {
296
+ load_texture_internal(texture_id, url, true);
297
+ }
298
+
299
+ pub fn acquire_svg_asset(url: &str) -> u32 {
300
+ if url.is_empty() {
301
+ return 0;
302
+ }
303
+ let svg_id = SVG_IDS_BY_URL
304
+ .with(|map| map.borrow().get(url).copied())
305
+ .unwrap_or_else(|| {
306
+ let svg_id = next_dynamic_svg_id();
307
+ load_svg_internal(svg_id, url, false);
308
+ svg_id
309
+ });
310
+ SVG_REF_COUNTS.with(|ref_counts| increment_ref_count(ref_counts, svg_id));
311
+ svg_id
312
+ }
313
+
314
+ pub fn acquire_texture_asset(url: &str) -> u32 {
315
+ if url.is_empty() {
316
+ return 0;
317
+ }
318
+ let texture_id = TEXTURE_IDS_BY_URL
319
+ .with(|map| map.borrow().get(url).copied())
320
+ .unwrap_or_else(|| {
321
+ let texture_id = next_dynamic_texture_id();
322
+ load_texture_internal(texture_id, url, false);
323
+ texture_id
324
+ });
325
+ TEXTURE_REF_COUNTS.with(|ref_counts| increment_ref_count(ref_counts, texture_id));
326
+ texture_id
327
+ }
328
+
329
+ pub fn release_svg_asset(svg_id: u32) {
330
+ if svg_id == 0 {
331
+ return;
332
+ }
333
+ let remaining = SVG_REF_COUNTS.with(|ref_counts| decrement_ref_count(ref_counts, svg_id));
334
+ let pinned = PINNED_SVG_IDS.with(|ids| ids.borrow().contains(&svg_id));
335
+ if remaining > 0 || remaining < 0 || pinned {
336
+ return;
337
+ }
338
+ let url = get_svg_record(svg_id).borrow().url.clone();
339
+ remove_url_binding(&SVG_IDS_BY_URL, &url, svg_id);
340
+ SVG_ASSETS.with(|records| {
341
+ records.borrow_mut().remove(&svg_id);
342
+ });
343
+ unsafe {
344
+ ffi::fui_release_svg(svg_id);
345
+ }
346
+ }
347
+
348
+ pub fn release_texture_asset(texture_id: u32) {
349
+ if texture_id == 0 {
350
+ return;
351
+ }
352
+ let remaining =
353
+ TEXTURE_REF_COUNTS.with(|ref_counts| decrement_ref_count(ref_counts, texture_id));
354
+ let pinned = PINNED_TEXTURE_IDS.with(|ids| ids.borrow().contains(&texture_id));
355
+ if remaining > 0 || remaining < 0 || pinned {
356
+ return;
357
+ }
358
+ let url = get_texture_record(texture_id).borrow().url.clone();
359
+ remove_url_binding(&TEXTURE_IDS_BY_URL, &url, texture_id);
360
+ TEXTURE_ASSETS.with(|records| {
361
+ records.borrow_mut().remove(&texture_id);
362
+ });
363
+ unsafe {
364
+ ffi::fui_release_texture(texture_id);
365
+ }
366
+ }
367
+
368
+ pub fn ensure_svg_asset(url: &str) -> u32 {
369
+ acquire_svg_asset(url)
370
+ }
371
+
372
+ pub fn ensure_texture_asset(url: &str) -> u32 {
373
+ acquire_texture_asset(url)
374
+ }
375
+
376
+ pub fn get_svg_asset_state(svg_id: u32) -> AssetStateSignal {
377
+ get_svg_record(svg_id).borrow().state.clone()
378
+ }
379
+
380
+ pub fn get_texture_asset_state(texture_id: u32) -> AssetStateSignal {
381
+ get_texture_record(texture_id).borrow().state.clone()
382
+ }
383
+
384
+ pub fn get_svg_asset_state_value(svg_id: u32) -> AssetLoadState {
385
+ get_svg_record(svg_id).borrow().state.get()
386
+ }
387
+
388
+ pub fn get_texture_asset_state_value(texture_id: u32) -> AssetLoadState {
389
+ get_texture_record(texture_id).borrow().state.get()
390
+ }
391
+
392
+ pub fn get_svg_asset_error(svg_id: u32) -> String {
393
+ get_svg_record(svg_id).borrow().error.clone()
394
+ }
395
+
396
+ pub fn get_svg_asset_url(svg_id: u32) -> String {
397
+ get_svg_record(svg_id).borrow().url.clone()
398
+ }
399
+
400
+ pub fn get_svg_asset_width(svg_id: u32) -> f32 {
401
+ get_svg_record(svg_id).borrow().width
402
+ }
403
+
404
+ pub fn get_svg_asset_height(svg_id: u32) -> f32 {
405
+ get_svg_record(svg_id).borrow().height
406
+ }
407
+
408
+ pub fn get_texture_asset_error(texture_id: u32) -> String {
409
+ get_texture_record(texture_id).borrow().error.clone()
410
+ }
411
+
412
+ pub fn get_texture_asset_url(texture_id: u32) -> String {
413
+ get_texture_record(texture_id).borrow().url.clone()
414
+ }
415
+
416
+ pub fn get_texture_asset_width(texture_id: u32) -> f32 {
417
+ get_texture_record(texture_id).borrow().width
418
+ }
419
+
420
+ pub fn get_texture_asset_height(texture_id: u32) -> f32 {
421
+ get_texture_record(texture_id).borrow().height
422
+ }
423
+
424
+ pub fn mark_texture_asset_ready(texture_id: u32, width: f32, height: f32) {
425
+ mark_loaded(&get_texture_record(texture_id), width, height);
426
+ }
427
+
428
+ pub fn on_svg_loaded(svg_id: u32, width: f32, height: f32) {
429
+ mark_loaded(&get_svg_record(svg_id), width, height);
430
+ }
431
+
432
+ pub fn on_svg_failed(svg_id: u32, error: String) {
433
+ let record = get_svg_record(svg_id);
434
+ let url = record.borrow().url.clone();
435
+ let message = if error.is_empty() {
436
+ "unknown error".to_string()
437
+ } else {
438
+ error.clone()
439
+ };
440
+ mark_failed(&record, error);
441
+ warn(
442
+ "Assets",
443
+ &format!("SVG load failed for \"{url}\": {message}"),
444
+ );
445
+ }
446
+
447
+ pub fn on_texture_loaded(texture_id: u32, width: f32, height: f32) {
448
+ mark_loaded(&get_texture_record(texture_id), width, height);
449
+ }
450
+
451
+ pub fn on_texture_failed(texture_id: u32, error: String) {
452
+ let record = get_texture_record(texture_id);
453
+ let url = record.borrow().url.clone();
454
+ let message = if error.is_empty() {
455
+ "unknown error".to_string()
456
+ } else {
457
+ error.clone()
458
+ };
459
+ mark_failed(&record, error);
460
+ warn(
461
+ "Assets",
462
+ &format!("Texture load failed for \"{url}\": {message}"),
463
+ );
464
+ }
465
+
466
+ #[cfg(test)]
467
+ pub(crate) fn test_reset() {
468
+ SVG_ASSETS.with(|slot| slot.borrow_mut().clear());
469
+ TEXTURE_ASSETS.with(|slot| slot.borrow_mut().clear());
470
+ SVG_IDS_BY_URL.with(|slot| slot.borrow_mut().clear());
471
+ TEXTURE_IDS_BY_URL.with(|slot| slot.borrow_mut().clear());
472
+ SVG_REF_COUNTS.with(|slot| slot.borrow_mut().clear());
473
+ TEXTURE_REF_COUNTS.with(|slot| slot.borrow_mut().clear());
474
+ PINNED_SVG_IDS.with(|slot| slot.borrow_mut().clear());
475
+ PINNED_TEXTURE_IDS.with(|slot| slot.borrow_mut().clear());
476
+ LOADED_FONT_IDS.with(|ids| {
477
+ *ids.borrow_mut() = default_loaded_font_ids();
478
+ });
479
+ NEXT_DYNAMIC_SVG_ID.with(|slot| slot.set(FIRST_DYNAMIC_SVG_ID));
480
+ NEXT_DYNAMIC_TEXTURE_ID.with(|slot| slot.set(FIRST_DYNAMIC_TEXTURE_ID));
481
+ }
482
+
483
+ #[cfg(test)]
484
+ mod tests {
485
+ use super::*;
486
+ use crate::ffi::{self, Call};
487
+ use std::cell::Cell;
488
+ use std::rc::Rc;
489
+
490
+ #[test]
491
+ fn asset_loaders_emit_host_calls() {
492
+ test_reset();
493
+ ffi::test::reset();
494
+
495
+ load_font(1, "/v2/fonts/NotoSans-Regular.ttf");
496
+ load_svg(2, "data:image/svg+xml,%3Csvg/%3E");
497
+ load_texture(3, "/v2/fui-as/demo/demo-texture.png");
498
+
499
+ let calls = ffi::test::take_calls();
500
+ assert!(calls.iter().any(|call| matches!(call, Call::LoadFont { font_id: 1, url } if url == "/v2/fonts/NotoSans-Regular.ttf")));
501
+ assert!(calls.iter().any(|call| matches!(call, Call::LoadSvg { svg_id: 2, url } if url == "data:image/svg+xml,%3Csvg/%3E")));
502
+ assert!(calls.iter().any(|call| matches!(call, Call::LoadTexture { texture_id: 3, url } if url == "/v2/fui-as/demo/demo-texture.png")));
503
+ }
504
+
505
+ #[test]
506
+ fn acquired_assets_share_url_and_release_when_last_ref_drops() {
507
+ test_reset();
508
+ ffi::test::reset();
509
+
510
+ let first = acquire_texture_asset("/img/a.png");
511
+ let second = acquire_texture_asset("/img/a.png");
512
+ assert_eq!(first, second);
513
+ let calls = ffi::test::take_calls();
514
+ let load_calls = calls
515
+ .iter()
516
+ .filter(|call| matches!(call, Call::LoadTexture { .. }))
517
+ .count();
518
+ assert_eq!(load_calls, 1);
519
+
520
+ release_texture_asset(first);
521
+ let calls = ffi::test::take_calls();
522
+ assert!(!calls.iter().any(
523
+ |call| matches!(call, Call::ReleaseTexture { texture_id } if *texture_id == first)
524
+ ));
525
+
526
+ release_texture_asset(second);
527
+ let calls = ffi::test::take_calls();
528
+ assert!(calls.iter().any(
529
+ |call| matches!(call, Call::ReleaseTexture { texture_id } if *texture_id == first)
530
+ ));
531
+ }
532
+
533
+ #[test]
534
+ fn pinned_assets_are_not_released_by_ref_count_drop() {
535
+ test_reset();
536
+ ffi::test::reset();
537
+
538
+ load_svg(55, "/img/pinned.svg");
539
+ let acquired = acquire_svg_asset("/img/pinned.svg");
540
+ assert_eq!(acquired, 55);
541
+ ffi::test::take_calls();
542
+
543
+ release_svg_asset(acquired);
544
+ let calls = ffi::test::take_calls();
545
+ assert!(!calls
546
+ .iter()
547
+ .any(|call| matches!(call, Call::ReleaseSvg { svg_id } if *svg_id == acquired)));
548
+ }
549
+
550
+ #[test]
551
+ fn asset_state_signal_notifies_and_tracks_ready_and_failed_payloads() {
552
+ test_reset();
553
+ let texture_id = acquire_texture_asset("/img/ready.png");
554
+ let state = get_texture_asset_state(texture_id);
555
+ assert_eq!(state.get(), AssetLoadState::Loading);
556
+
557
+ let hits = Rc::new(Cell::new(0));
558
+ let hits_clone = hits.clone();
559
+ let callback: Callback = Rc::new(move || hits_clone.set(hits_clone.get() + 1));
560
+ let _guard = state.subscribe(callback);
561
+
562
+ on_texture_loaded(texture_id, 64.0, 32.0);
563
+ assert_eq!(state.get(), AssetLoadState::Ready);
564
+ assert_eq!(get_texture_asset_width(texture_id), 64.0);
565
+ assert_eq!(get_texture_asset_height(texture_id), 32.0);
566
+
567
+ on_texture_failed(texture_id, "broken".to_string());
568
+ assert_eq!(state.get(), AssetLoadState::Failed);
569
+ assert_eq!(get_texture_asset_error(texture_id), "broken");
570
+ assert_eq!(hits.get(), 2);
571
+ }
572
+ }
@@ -0,0 +1 @@
1
+ pub mod ui;