@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/fetch.rs ADDED
@@ -0,0 +1,500 @@
1
+ use std::cell::RefCell;
2
+ use std::collections::HashMap;
3
+ use std::rc::Rc;
4
+
5
+ use crate::ffi;
6
+
7
+ type ResponseCallback = Rc<dyn Fn(FetchResponse)>;
8
+ type ErrorCallback = Rc<dyn Fn(FetchErrorEventArgs)>;
9
+
10
+ thread_local! {
11
+ static NEXT_FETCH_ID: RefCell<u32> = const { RefCell::new(1) };
12
+ static ACTIVE_REQUESTS: RefCell<HashMap<u32, Rc<RefCell<FetchRequestInner>>>> = RefCell::new(HashMap::new());
13
+ }
14
+
15
+ fn encode_text_parts(values: &[String]) -> Vec<u8> {
16
+ let mut bytes = Vec::new();
17
+ bytes.extend_from_slice(&(values.len() as u32).to_le_bytes());
18
+ for value in values {
19
+ let encoded = value.as_bytes();
20
+ bytes.extend_from_slice(&(encoded.len() as u32).to_le_bytes());
21
+ bytes.extend_from_slice(encoded);
22
+ }
23
+ bytes
24
+ }
25
+
26
+ fn decode_text_parts(bytes: &[u8]) -> Vec<String> {
27
+ if bytes.len() < 4 {
28
+ return Vec::new();
29
+ }
30
+ let mut cursor = 0usize;
31
+ let count =
32
+ u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0, 0, 0, 0])) as usize;
33
+ cursor += 4;
34
+ let mut values = Vec::with_capacity(count);
35
+ for _ in 0..count {
36
+ if cursor + 4 > bytes.len() {
37
+ break;
38
+ }
39
+ let len = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0, 0, 0, 0]))
40
+ as usize;
41
+ cursor += 4;
42
+ if cursor + len > bytes.len() {
43
+ break;
44
+ }
45
+ values.push(String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned());
46
+ cursor += len;
47
+ }
48
+ values
49
+ }
50
+
51
+ fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
52
+ let bytes = value.as_bytes();
53
+ callback(
54
+ if bytes.is_empty() {
55
+ 0
56
+ } else {
57
+ bytes.as_ptr() as usize
58
+ },
59
+ bytes.len() as u32,
60
+ );
61
+ }
62
+
63
+ #[derive(Clone, Debug, PartialEq, Eq)]
64
+ pub struct FetchResponse {
65
+ pub ok: bool,
66
+ pub status: i32,
67
+ pub status_text: String,
68
+ pub url: String,
69
+ }
70
+
71
+ #[derive(Clone, Debug, PartialEq, Eq)]
72
+ pub struct FetchErrorEventArgs {
73
+ pub message: String,
74
+ }
75
+
76
+ struct FetchRequestInner {
77
+ method: String,
78
+ url: String,
79
+ headers: Vec<String>,
80
+ body: Vec<u8>,
81
+ on_complete: Option<ResponseCallback>,
82
+ on_error: Option<ErrorCallback>,
83
+ request_id: u32,
84
+ started: bool,
85
+ finished: bool,
86
+ }
87
+
88
+ pub struct FetchRequest {
89
+ inner: Rc<RefCell<FetchRequestInner>>,
90
+ }
91
+
92
+ impl FetchRequest {
93
+ fn new(url: impl Into<String>) -> Self {
94
+ Self {
95
+ inner: Rc::new(RefCell::new(FetchRequestInner {
96
+ method: "GET".to_string(),
97
+ url: url.into(),
98
+ headers: Vec::new(),
99
+ body: Vec::new(),
100
+ on_complete: None,
101
+ on_error: None,
102
+ request_id: 0,
103
+ started: false,
104
+ finished: false,
105
+ })),
106
+ }
107
+ }
108
+
109
+ pub fn method(self, value: impl Into<String>) -> Self {
110
+ self.inner.borrow_mut().method = value.into();
111
+ self
112
+ }
113
+
114
+ pub fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
115
+ let mut inner = self.inner.borrow_mut();
116
+ inner.headers.push(name.into());
117
+ inner.headers.push(value.into());
118
+ drop(inner);
119
+ self
120
+ }
121
+
122
+ pub fn body_bytes(self, value: Vec<u8>) -> Self {
123
+ self.inner.borrow_mut().body = value;
124
+ self
125
+ }
126
+
127
+ pub fn body_text(self, value: impl Into<String>) -> Self {
128
+ self.body_bytes(value.into().into_bytes())
129
+ }
130
+
131
+ pub fn on_complete(self, handler: impl Fn(FetchResponse) + 'static) -> Self {
132
+ self.inner.borrow_mut().on_complete = Some(Rc::new(handler));
133
+ self
134
+ }
135
+
136
+ pub fn on_error(self, handler: impl Fn(FetchErrorEventArgs) + 'static) -> Self {
137
+ self.inner.borrow_mut().on_error = Some(Rc::new(handler));
138
+ self
139
+ }
140
+
141
+ pub fn start(self) -> Self {
142
+ let already_started = {
143
+ let inner = self.inner.borrow();
144
+ inner.started || inner.finished
145
+ };
146
+ if already_started {
147
+ return self;
148
+ }
149
+ let empty_url_error = {
150
+ let inner = self.inner.borrow();
151
+ if inner.url.is_empty() {
152
+ inner.on_error.clone()
153
+ } else {
154
+ None
155
+ }
156
+ };
157
+ if self.inner.borrow().url.is_empty() {
158
+ if let Some(callback) = empty_url_error {
159
+ callback(FetchErrorEventArgs {
160
+ message: "FetchRequest.start: url must not be empty.".to_string(),
161
+ });
162
+ }
163
+ return self;
164
+ }
165
+ let (method, url, headers, body, request_id) = {
166
+ let mut inner = self.inner.borrow_mut();
167
+ let request_id = NEXT_FETCH_ID.with(|next| {
168
+ let mut slot = next.borrow_mut();
169
+ let id = *slot;
170
+ *slot += 1;
171
+ id
172
+ });
173
+ inner.request_id = request_id;
174
+ inner.started = true;
175
+ ACTIVE_REQUESTS.with(|requests| {
176
+ requests.borrow_mut().insert(request_id, self.inner.clone());
177
+ });
178
+ (
179
+ inner.method.clone(),
180
+ inner.url.clone(),
181
+ inner.headers.clone(),
182
+ inner.body.clone(),
183
+ request_id,
184
+ )
185
+ };
186
+ let header_bytes = encode_text_parts(&headers);
187
+ with_utf8(&method, |method_ptr, method_len| {
188
+ with_utf8(&url, |url_ptr, url_len| unsafe {
189
+ ffi::fui_fetch_start(
190
+ request_id,
191
+ method_ptr,
192
+ method_len,
193
+ url_ptr,
194
+ url_len,
195
+ if header_bytes.is_empty() {
196
+ 0
197
+ } else {
198
+ header_bytes.as_ptr() as usize
199
+ },
200
+ header_bytes.len() as u32,
201
+ if body.is_empty() {
202
+ 0
203
+ } else {
204
+ body.as_ptr() as usize
205
+ },
206
+ body.len() as u32,
207
+ );
208
+ })
209
+ });
210
+ self
211
+ }
212
+
213
+ pub fn cancel(&self) {
214
+ let request_id = {
215
+ let inner = self.inner.borrow();
216
+ if !inner.started || inner.finished || inner.request_id == 0 {
217
+ return;
218
+ }
219
+ inner.request_id
220
+ };
221
+ unsafe { ffi::fui_fetch_cancel(request_id) };
222
+ finish_request(&self.inner);
223
+ }
224
+ }
225
+
226
+ impl Drop for FetchRequest {
227
+ fn drop(&mut self) {
228
+ self.cancel();
229
+ }
230
+ }
231
+
232
+ fn finish_request(request: &Rc<RefCell<FetchRequestInner>>) {
233
+ let request_id = {
234
+ let mut inner = request.borrow_mut();
235
+ if inner.finished {
236
+ return;
237
+ }
238
+ let request_id = inner.request_id;
239
+ inner.request_id = 0;
240
+ inner.finished = true;
241
+ inner.on_complete = None;
242
+ inner.on_error = None;
243
+ request_id
244
+ };
245
+ if request_id != 0 {
246
+ ACTIVE_REQUESTS.with(|requests| {
247
+ requests.borrow_mut().remove(&request_id);
248
+ });
249
+ }
250
+ }
251
+
252
+ pub struct Fetch;
253
+
254
+ impl Fetch {
255
+ pub fn request(url: impl Into<String>) -> FetchRequest {
256
+ FetchRequest::new(url)
257
+ }
258
+ }
259
+
260
+ fn complete_request(request_id: u32, response: FetchResponse) {
261
+ let request = ACTIVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
262
+ let Some(request) = request else {
263
+ return;
264
+ };
265
+ let callback = {
266
+ let mut inner = request.borrow_mut();
267
+ if inner.finished {
268
+ return;
269
+ }
270
+ inner.finished = true;
271
+ inner.request_id = 0;
272
+ inner.on_error = None;
273
+ inner.on_complete.clone()
274
+ };
275
+ request.borrow_mut().on_complete = None;
276
+ if let Some(callback) = callback {
277
+ callback(response);
278
+ }
279
+ }
280
+
281
+ fn fail_request(request_id: u32, message: String) {
282
+ let request = ACTIVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
283
+ let Some(request) = request else {
284
+ return;
285
+ };
286
+ let callback = {
287
+ let mut inner = request.borrow_mut();
288
+ if inner.finished {
289
+ return;
290
+ }
291
+ inner.finished = true;
292
+ inner.request_id = 0;
293
+ inner.on_complete = None;
294
+ inner.on_error.clone()
295
+ };
296
+ request.borrow_mut().on_error = None;
297
+ if let Some(callback) = callback {
298
+ callback(FetchErrorEventArgs { message });
299
+ }
300
+ }
301
+
302
+ pub fn dispose_all_fetch_requests() {
303
+ let requests = ACTIVE_REQUESTS.with(|requests| {
304
+ requests
305
+ .borrow_mut()
306
+ .drain()
307
+ .map(|(_, request)| request)
308
+ .collect::<Vec<_>>()
309
+ });
310
+ for request in requests {
311
+ let request_id = {
312
+ let mut inner = request.borrow_mut();
313
+ if inner.finished || !inner.started || inner.request_id == 0 {
314
+ inner.finished = true;
315
+ inner.on_complete = None;
316
+ inner.on_error = None;
317
+ 0
318
+ } else {
319
+ let request_id = inner.request_id;
320
+ inner.request_id = 0;
321
+ inner.finished = true;
322
+ inner.on_complete = None;
323
+ inner.on_error = None;
324
+ request_id
325
+ }
326
+ };
327
+ if request_id != 0 {
328
+ unsafe { ffi::fui_fetch_cancel(request_id) };
329
+ }
330
+ }
331
+ }
332
+
333
+ pub fn reset_fetch_runtime() {
334
+ dispose_all_fetch_requests();
335
+ NEXT_FETCH_ID.with(|next| {
336
+ *next.borrow_mut() = 1;
337
+ });
338
+ }
339
+
340
+ #[no_mangle]
341
+ pub extern "C" fn __fui_on_fetch_complete(
342
+ request_id: u32,
343
+ ok: bool,
344
+ status: i32,
345
+ payload_ptr: *const u8,
346
+ payload_len: u32,
347
+ ) {
348
+ let parts = if payload_ptr.is_null() || payload_len == 0 {
349
+ Vec::new()
350
+ } else {
351
+ decode_text_parts(unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) })
352
+ };
353
+ complete_request(
354
+ request_id,
355
+ FetchResponse {
356
+ ok,
357
+ status,
358
+ status_text: parts.first().cloned().unwrap_or_default(),
359
+ url: parts.get(1).cloned().unwrap_or_default(),
360
+ },
361
+ );
362
+ }
363
+
364
+ #[no_mangle]
365
+ pub extern "C" fn __fui_on_fetch_error(request_id: u32, payload_ptr: *const u8, payload_len: u32) {
366
+ let message = if payload_ptr.is_null() || payload_len == 0 {
367
+ "Fetch request failed.".to_string()
368
+ } else {
369
+ String::from_utf8_lossy(unsafe {
370
+ std::slice::from_raw_parts(payload_ptr, payload_len as usize)
371
+ })
372
+ .into_owned()
373
+ };
374
+ fail_request(request_id, message);
375
+ }
376
+
377
+ #[cfg(test)]
378
+ mod tests {
379
+ use super::Fetch;
380
+ use crate::ffi::{self, Call};
381
+ use std::cell::RefCell;
382
+ use std::rc::Rc;
383
+
384
+ #[test]
385
+ fn fetch_request_emits_host_call() {
386
+ ffi::test::reset();
387
+ let request = Fetch::request("https://example.com")
388
+ .method("POST")
389
+ .header("Accept", "application/json")
390
+ .body_text("hello")
391
+ .start();
392
+ let calls = ffi::test::take_calls();
393
+ assert!(calls.iter().any(|call| matches!(call, Call::FetchStart { method, url, .. } if method == "POST" && url == "https://example.com")));
394
+ drop(request);
395
+ }
396
+
397
+ #[test]
398
+ fn fetch_callbacks_receive_result() {
399
+ ffi::test::reset();
400
+ let result = Rc::new(RefCell::new(String::new()));
401
+ let result_clone = result.clone();
402
+ let request = Fetch::request("https://example.com")
403
+ .on_complete(move |response| {
404
+ result_clone.replace(response.status_text);
405
+ })
406
+ .start();
407
+ let payload = {
408
+ let mut bytes = Vec::new();
409
+ bytes.extend_from_slice(&2u32.to_le_bytes());
410
+ for part in ["OK", "https://example.com"] {
411
+ bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
412
+ bytes.extend_from_slice(part.as_bytes());
413
+ }
414
+ bytes
415
+ };
416
+ super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
417
+ assert_eq!(&*result.borrow(), "OK");
418
+ drop(request);
419
+ }
420
+
421
+ #[test]
422
+ fn fetch_empty_url_reports_error_without_host_call() {
423
+ ffi::test::reset();
424
+ super::reset_fetch_runtime();
425
+ let result = Rc::new(RefCell::new(String::new()));
426
+ let result_clone = result.clone();
427
+ let request = Fetch::request("")
428
+ .on_error(move |event| {
429
+ result_clone.replace(event.message);
430
+ })
431
+ .start();
432
+ assert_eq!(
433
+ &*result.borrow(),
434
+ "FetchRequest.start: url must not be empty."
435
+ );
436
+ assert!(ffi::test::take_calls()
437
+ .iter()
438
+ .all(|call| !matches!(call, Call::FetchStart { .. })));
439
+ drop(request);
440
+ }
441
+
442
+ #[test]
443
+ fn fetch_cancel_finishes_and_suppresses_late_completion() {
444
+ ffi::test::reset();
445
+ super::reset_fetch_runtime();
446
+ let result = Rc::new(RefCell::new(String::new()));
447
+ let result_clone = result.clone();
448
+ let request = Fetch::request("https://example.com")
449
+ .on_complete(move |response| {
450
+ result_clone.replace(response.status_text);
451
+ })
452
+ .start();
453
+ request.cancel();
454
+ let calls = ffi::test::take_calls();
455
+ assert!(calls
456
+ .iter()
457
+ .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
458
+ let payload = {
459
+ let mut bytes = Vec::new();
460
+ bytes.extend_from_slice(&2u32.to_le_bytes());
461
+ for part in ["OK", "https://example.com"] {
462
+ bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
463
+ bytes.extend_from_slice(part.as_bytes());
464
+ }
465
+ bytes
466
+ };
467
+ super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
468
+ assert_eq!(&*result.borrow(), "");
469
+ drop(request);
470
+ }
471
+
472
+ #[test]
473
+ fn fetch_drop_cancels_active_request() {
474
+ ffi::test::reset();
475
+ super::reset_fetch_runtime();
476
+ {
477
+ let _request = Fetch::request("https://example.com").start();
478
+ }
479
+ let calls = ffi::test::take_calls();
480
+ assert!(calls
481
+ .iter()
482
+ .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
483
+ }
484
+
485
+ #[test]
486
+ fn fetch_error_callback_receives_default_message() {
487
+ ffi::test::reset();
488
+ super::reset_fetch_runtime();
489
+ let result = Rc::new(RefCell::new(String::new()));
490
+ let result_clone = result.clone();
491
+ let request = Fetch::request("https://example.com")
492
+ .on_error(move |event| {
493
+ result_clone.replace(event.message);
494
+ })
495
+ .start();
496
+ super::__fui_on_fetch_error(1, std::ptr::null(), 0);
497
+ assert_eq!(&*result.borrow(), "Fetch request failed.");
498
+ drop(request);
499
+ }
500
+ }