@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,41 @@
1
+ use crate::animation::AnimationTiming;
2
+
3
+ #[derive(Clone, Debug, Default, PartialEq)]
4
+ pub struct NodeTransitions {
5
+ opacity_timing: Option<AnimationTiming>,
6
+ background_color_timing: Option<AnimationTiming>,
7
+ scroll_offset_timing: Option<AnimationTiming>,
8
+ }
9
+
10
+ impl NodeTransitions {
11
+ pub fn new() -> Self {
12
+ Self::default()
13
+ }
14
+
15
+ pub fn opacity(mut self, timing: AnimationTiming) -> Self {
16
+ self.opacity_timing = Some(timing);
17
+ self
18
+ }
19
+
20
+ pub fn bg_color(mut self, timing: AnimationTiming) -> Self {
21
+ self.background_color_timing = Some(timing);
22
+ self
23
+ }
24
+
25
+ pub fn scroll_offset(mut self, timing: AnimationTiming) -> Self {
26
+ self.scroll_offset_timing = Some(timing);
27
+ self
28
+ }
29
+
30
+ pub(crate) fn opacity_timing(&self) -> Option<AnimationTiming> {
31
+ self.opacity_timing
32
+ }
33
+
34
+ pub(crate) fn background_color_timing(&self) -> Option<AnimationTiming> {
35
+ self.background_color_timing
36
+ }
37
+
38
+ pub(crate) fn scroll_offset_timing(&self) -> Option<AnimationTiming> {
39
+ self.scroll_offset_timing
40
+ }
41
+ }
@@ -0,0 +1,520 @@
1
+ use crate::assets;
2
+ use crate::bindings::ui;
3
+ use crate::logger::warn;
4
+ use std::cell::RefCell;
5
+ use std::rc::Rc;
6
+
7
+ const STYLE_MISMATCH_PENALTY: i32 = 1000;
8
+ const MAX_FONT_SCORE: i32 = i32::MAX;
9
+ const FIRST_DYNAMIC_FONT_ID: u32 = 1024;
10
+
11
+ thread_local! {
12
+ static NEXT_DYNAMIC_FONT_ID: RefCell<u32> = const { RefCell::new(FIRST_DYNAMIC_FONT_ID) };
13
+ static FONT_LOADED_CALLBACKS: RefCell<Vec<FontLoadedRegistration>> = const { RefCell::new(Vec::new()) };
14
+ static FONT_READY_CALLBACKS: RefCell<Vec<FontReadyRegistration>> = const { RefCell::new(Vec::new()) };
15
+ static REGISTERED_FONT_FALLBACKS: RefCell<Vec<(u32, u32)>> = const { RefCell::new(Vec::new()) };
16
+ }
17
+
18
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
19
+ pub enum FontStyle {
20
+ Normal = 0,
21
+ Italic = 1,
22
+ }
23
+
24
+ impl Default for FontStyle {
25
+ fn default() -> Self {
26
+ Self::Normal
27
+ }
28
+ }
29
+
30
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
31
+ pub enum FontWeight {
32
+ Regular = 400,
33
+ Medium = 500,
34
+ Semibold = 600,
35
+ Bold = 700,
36
+ }
37
+
38
+ impl Default for FontWeight {
39
+ fn default() -> Self {
40
+ Self::Regular
41
+ }
42
+ }
43
+
44
+ #[derive(Clone, Debug, PartialEq, Eq)]
45
+ pub struct FontFaceLoadedEventArgs {
46
+ pub font: FontFace,
47
+ }
48
+
49
+ #[derive(Clone, Debug, PartialEq, Eq)]
50
+ pub struct FontsLoadedEventArgs;
51
+
52
+ #[derive(Clone, Debug, PartialEq, Eq)]
53
+ pub struct FontStackLoadedEventArgs {
54
+ pub stack: FontStack,
55
+ }
56
+
57
+ struct FontLoadedRegistration {
58
+ font_id: u32,
59
+ callback: Rc<dyn Fn(FontFaceLoadedEventArgs)>,
60
+ }
61
+
62
+ struct FontReadyRegistration {
63
+ font_ids: Vec<u32>,
64
+ callback: Rc<dyn Fn()>,
65
+ }
66
+
67
+ fn abs_i32(value: i32) -> i32 {
68
+ if value < 0 {
69
+ -value
70
+ } else {
71
+ value
72
+ }
73
+ }
74
+
75
+ fn allocate_dynamic_font_id() -> u32 {
76
+ NEXT_DYNAMIC_FONT_ID.with(|slot| {
77
+ let mut next = slot.borrow_mut();
78
+ let allocated = *next;
79
+ *next += 1;
80
+ allocated
81
+ })
82
+ }
83
+
84
+ fn push_unique_font_id(font_ids: &mut Vec<u32>, font_id: u32) {
85
+ if font_id == 0 || font_ids.contains(&font_id) {
86
+ return;
87
+ }
88
+ font_ids.push(font_id);
89
+ }
90
+
91
+ fn normalize_font_ids(font_ids: &[u32]) -> Vec<u32> {
92
+ let mut unique = Vec::with_capacity(font_ids.len());
93
+ for font_id in font_ids {
94
+ push_unique_font_id(&mut unique, *font_id);
95
+ }
96
+ unique
97
+ }
98
+
99
+ fn register_font_fallback_once(font_id: u32, fallback_font_id: u32) {
100
+ if font_id == 0 || fallback_font_id == 0 {
101
+ return;
102
+ }
103
+ let already_registered = REGISTERED_FONT_FALLBACKS.with(|pairs| {
104
+ pairs
105
+ .borrow()
106
+ .iter()
107
+ .any(|(registered_font_id, registered_fallback_id)| {
108
+ *registered_font_id == font_id && *registered_fallback_id == fallback_font_id
109
+ })
110
+ });
111
+ if already_registered {
112
+ return;
113
+ }
114
+ ui::register_font_fallback(font_id, fallback_font_id);
115
+ REGISTERED_FONT_FALLBACKS.with(|pairs| {
116
+ pairs.borrow_mut().push((font_id, fallback_font_id));
117
+ });
118
+ }
119
+
120
+ fn are_font_ids_loaded(font_ids: &[u32]) -> bool {
121
+ font_ids
122
+ .iter()
123
+ .all(|font_id| FontFace::is_font_loaded(*font_id))
124
+ }
125
+
126
+ #[derive(Clone, Debug, PartialEq, Eq)]
127
+ pub struct FontFace {
128
+ id: u32,
129
+ }
130
+
131
+ impl FontFace {
132
+ pub(crate) fn new(id: u32) -> Self {
133
+ Self { id }
134
+ }
135
+
136
+ pub fn load(url: &str) -> Self {
137
+ Self::load_with_id(url, allocate_dynamic_font_id())
138
+ }
139
+
140
+ pub(crate) fn load_with_id(url: &str, id: u32) -> Self {
141
+ Self::new(id).load_into(url)
142
+ }
143
+
144
+ pub(crate) fn id(&self) -> u32 {
145
+ self.id
146
+ }
147
+
148
+ fn load_into(self, url: &str) -> Self {
149
+ if url.is_empty() {
150
+ warn("Typography", "FontFace.load() received an empty font URL.");
151
+ }
152
+ assets::load_font(self.id, url);
153
+ self
154
+ }
155
+
156
+ pub fn is_loaded(&self) -> bool {
157
+ Self::is_font_loaded(self.id)
158
+ }
159
+
160
+ pub(crate) fn is_font_loaded(font_id: u32) -> bool {
161
+ font_id == 0 || assets::is_font_loaded(font_id)
162
+ }
163
+
164
+ pub fn on_loaded(&self, callback: impl Fn(FontFaceLoadedEventArgs) + 'static) -> Self {
165
+ Self::when_loaded(self.id, callback);
166
+ self.clone()
167
+ }
168
+
169
+ pub(crate) fn when_loaded(font_id: u32, callback: impl Fn(FontFaceLoadedEventArgs) + 'static) {
170
+ if Self::is_font_loaded(font_id) {
171
+ callback(FontFaceLoadedEventArgs {
172
+ font: FontFace::new(font_id),
173
+ });
174
+ return;
175
+ }
176
+ FONT_LOADED_CALLBACKS.with(|registrations| {
177
+ registrations.borrow_mut().push(FontLoadedRegistration {
178
+ font_id,
179
+ callback: Rc::new(callback),
180
+ });
181
+ });
182
+ }
183
+
184
+ pub(crate) fn when_fonts_loaded(
185
+ font_ids: &[u32],
186
+ callback: impl Fn(FontsLoadedEventArgs) + 'static,
187
+ ) {
188
+ let unique_font_ids = normalize_font_ids(font_ids);
189
+ if are_font_ids_loaded(&unique_font_ids) {
190
+ callback(FontsLoadedEventArgs);
191
+ return;
192
+ }
193
+ FONT_READY_CALLBACKS.with(|registrations| {
194
+ registrations.borrow_mut().push(FontReadyRegistration {
195
+ font_ids: unique_font_ids,
196
+ callback: Rc::new(move || callback(FontsLoadedEventArgs)),
197
+ });
198
+ });
199
+ }
200
+ }
201
+
202
+ #[derive(Clone, Debug, PartialEq, Eq)]
203
+ pub struct FontStack {
204
+ id: u32,
205
+ fallback_ids: Vec<u32>,
206
+ }
207
+
208
+ impl FontStack {
209
+ pub fn new(face: FontFace) -> Self {
210
+ Self {
211
+ id: face.id(),
212
+ fallback_ids: Vec::new(),
213
+ }
214
+ }
215
+
216
+ pub(crate) fn from_id(id: u32) -> Self {
217
+ Self::new(FontFace::new(id))
218
+ }
219
+
220
+ pub fn load(url: &str) -> Self {
221
+ Self::new(FontFace::load(url))
222
+ }
223
+
224
+ pub(crate) fn id(&self) -> u32 {
225
+ self.id
226
+ }
227
+
228
+ pub fn fallback_face(mut self, face: FontFace) -> Self {
229
+ self = self.fallback_id(face.id());
230
+ self
231
+ }
232
+
233
+ pub fn fallback_stack(mut self, stack: FontStack) -> Self {
234
+ self = self.fallback_id(stack.id());
235
+ self
236
+ }
237
+
238
+ pub fn fallback_loaded(self, url: &str) -> Self {
239
+ self.fallback_loaded_with_id(url, allocate_dynamic_font_id())
240
+ }
241
+
242
+ pub(crate) fn fallback_loaded_with_id(mut self, url: &str, font_id: u32) -> Self {
243
+ if url.is_empty() {
244
+ warn(
245
+ "Typography",
246
+ "FontStack.fallback_loaded() received an empty font URL.",
247
+ );
248
+ }
249
+ assets::load_font(font_id, url);
250
+ self = self.fallback_id(font_id);
251
+ self
252
+ }
253
+
254
+ pub(crate) fn required_font_ids(&self) -> Vec<u32> {
255
+ let mut font_ids = Vec::with_capacity(1 + self.fallback_ids.len());
256
+ push_unique_font_id(&mut font_ids, self.id);
257
+ for fallback_id in &self.fallback_ids {
258
+ push_unique_font_id(&mut font_ids, *fallback_id);
259
+ }
260
+ font_ids
261
+ }
262
+
263
+ pub fn is_loaded(&self) -> bool {
264
+ are_font_ids_loaded(&self.required_font_ids())
265
+ }
266
+
267
+ pub fn on_loaded(&self, callback: impl Fn(FontStackLoadedEventArgs) + 'static) -> Self {
268
+ let stack = self.clone();
269
+ let required = self.required_font_ids();
270
+ FontFace::when_fonts_loaded(&required, move |_| {
271
+ callback(FontStackLoadedEventArgs {
272
+ stack: stack.clone(),
273
+ });
274
+ });
275
+ self.clone()
276
+ }
277
+
278
+ fn fallback_id(mut self, font_id: u32) -> Self {
279
+ if font_id == 0 || font_id == self.id {
280
+ warn(
281
+ "Typography",
282
+ &format!(
283
+ "FontStack.fallback() ignored font id {} for stack {}.",
284
+ font_id, self.id
285
+ ),
286
+ );
287
+ return self;
288
+ }
289
+ if self.fallback_ids.contains(&font_id) {
290
+ return self;
291
+ }
292
+ register_font_fallback_once(self.id, font_id);
293
+ self.fallback_ids.push(font_id);
294
+ self
295
+ }
296
+ }
297
+
298
+ #[derive(Clone, Debug, PartialEq, Eq)]
299
+ pub struct FontFamily {
300
+ pub regular_stack: FontStack,
301
+ pub bold_stack: Option<FontStack>,
302
+ pub italic_stack: Option<FontStack>,
303
+ pub bold_italic_stack: Option<FontStack>,
304
+ pub medium_stack: Option<FontStack>,
305
+ pub medium_italic_stack: Option<FontStack>,
306
+ pub semibold_stack: Option<FontStack>,
307
+ pub semibold_italic_stack: Option<FontStack>,
308
+ }
309
+
310
+ impl FontFamily {
311
+ pub fn new(
312
+ regular_stack: FontStack,
313
+ bold_stack: Option<FontStack>,
314
+ italic_stack: Option<FontStack>,
315
+ bold_italic_stack: Option<FontStack>,
316
+ medium_stack: Option<FontStack>,
317
+ medium_italic_stack: Option<FontStack>,
318
+ semibold_stack: Option<FontStack>,
319
+ semibold_italic_stack: Option<FontStack>,
320
+ ) -> Self {
321
+ Self {
322
+ regular_stack,
323
+ bold_stack,
324
+ italic_stack,
325
+ bold_italic_stack,
326
+ medium_stack,
327
+ medium_italic_stack,
328
+ semibold_stack,
329
+ semibold_italic_stack,
330
+ }
331
+ }
332
+
333
+ #[cfg(test)]
334
+ pub(crate) fn from_ids(
335
+ regular: u32,
336
+ bold: u32,
337
+ italic: u32,
338
+ bold_italic: u32,
339
+ medium: u32,
340
+ medium_italic: u32,
341
+ semibold: u32,
342
+ semibold_italic: u32,
343
+ ) -> Self {
344
+ Self::new(
345
+ FontStack::from_id(regular),
346
+ (bold != 0).then(|| FontStack::from_id(bold)),
347
+ (italic != 0).then(|| FontStack::from_id(italic)),
348
+ (bold_italic != 0).then(|| FontStack::from_id(bold_italic)),
349
+ (medium != 0).then(|| FontStack::from_id(medium)),
350
+ (medium_italic != 0).then(|| FontStack::from_id(medium_italic)),
351
+ (semibold != 0).then(|| FontStack::from_id(semibold)),
352
+ (semibold_italic != 0).then(|| FontStack::from_id(semibold_italic)),
353
+ )
354
+ }
355
+
356
+ pub fn with_regular_stack(regular: FontStack) -> Self {
357
+ Self::new(regular, None, None, None, None, None, None, None)
358
+ }
359
+
360
+ pub fn with_regular_face(regular: FontFace) -> Self {
361
+ Self::with_regular_stack(FontStack::new(regular))
362
+ }
363
+
364
+ pub fn regular_bold_stacks(regular: FontStack, bold: FontStack) -> Self {
365
+ Self::new(regular, Some(bold), None, None, None, None, None, None)
366
+ }
367
+
368
+ pub(crate) fn italic_stack(mut self, stack: FontStack) -> Self {
369
+ self.italic_stack = Some(stack);
370
+ self
371
+ }
372
+
373
+ pub(crate) fn bold_italic_stack(mut self, stack: FontStack) -> Self {
374
+ self.bold_italic_stack = Some(stack);
375
+ self
376
+ }
377
+
378
+ pub(crate) fn resolve(&self, weight: FontWeight, style: FontStyle) -> u32 {
379
+ let target_weight = weight as i32;
380
+ let candidates = [
381
+ Some((&self.regular_stack, FontWeight::Regular, FontStyle::Normal)),
382
+ self.bold_stack
383
+ .as_ref()
384
+ .map(|stack| (stack, FontWeight::Bold, FontStyle::Normal)),
385
+ self.italic_stack
386
+ .as_ref()
387
+ .map(|stack| (stack, FontWeight::Regular, FontStyle::Italic)),
388
+ self.bold_italic_stack
389
+ .as_ref()
390
+ .map(|stack| (stack, FontWeight::Bold, FontStyle::Italic)),
391
+ self.medium_stack
392
+ .as_ref()
393
+ .map(|stack| (stack, FontWeight::Medium, FontStyle::Normal)),
394
+ self.medium_italic_stack
395
+ .as_ref()
396
+ .map(|stack| (stack, FontWeight::Medium, FontStyle::Italic)),
397
+ self.semibold_stack
398
+ .as_ref()
399
+ .map(|stack| (stack, FontWeight::Semibold, FontStyle::Normal)),
400
+ self.semibold_italic_stack
401
+ .as_ref()
402
+ .map(|stack| (stack, FontWeight::Semibold, FontStyle::Italic)),
403
+ ];
404
+
405
+ let mut best_id = 0;
406
+ let mut best_score = MAX_FONT_SCORE;
407
+ for (stack, candidate_weight, candidate_style) in candidates.into_iter().flatten() {
408
+ let score = Self::score_candidate(
409
+ stack.id(),
410
+ candidate_weight,
411
+ candidate_style,
412
+ target_weight,
413
+ style,
414
+ );
415
+ if score < best_score {
416
+ best_score = score;
417
+ best_id = stack.id();
418
+ }
419
+ }
420
+ if best_id == 0 {
421
+ warn(
422
+ "Typography",
423
+ &format!(
424
+ "FontFamily.resolve() could not resolve a font face for weight {} and style {}; the text will use font id 0.",
425
+ weight as i32,
426
+ style as u32,
427
+ ),
428
+ );
429
+ }
430
+ best_id
431
+ }
432
+
433
+ fn score_candidate(
434
+ font_id: u32,
435
+ weight: FontWeight,
436
+ style: FontStyle,
437
+ target_weight: i32,
438
+ target_style: FontStyle,
439
+ ) -> i32 {
440
+ if font_id == 0 {
441
+ return MAX_FONT_SCORE;
442
+ }
443
+ let mut score = abs_i32(weight as i32 - target_weight);
444
+ if style != target_style {
445
+ score += STYLE_MISMATCH_PENALTY;
446
+ }
447
+ score
448
+ }
449
+ }
450
+
451
+ pub(crate) fn notify_font_loaded(font_id: u32) {
452
+ FONT_LOADED_CALLBACKS.with(|registrations| {
453
+ let callbacks: Vec<Rc<dyn Fn(FontFaceLoadedEventArgs)>> = registrations
454
+ .borrow()
455
+ .iter()
456
+ .filter(|registration| registration.font_id == font_id)
457
+ .map(|registration| registration.callback.clone())
458
+ .collect();
459
+ for callback in callbacks {
460
+ callback(FontFaceLoadedEventArgs {
461
+ font: FontFace::new(font_id),
462
+ });
463
+ }
464
+ });
465
+
466
+ FONT_READY_CALLBACKS.with(|registrations| {
467
+ let mut registrations = registrations.borrow_mut();
468
+ let mut ready_callbacks = Vec::new();
469
+ registrations.retain(|registration| {
470
+ if are_font_ids_loaded(&registration.font_ids) {
471
+ ready_callbacks.push(registration.callback.clone());
472
+ false
473
+ } else {
474
+ true
475
+ }
476
+ });
477
+ drop(registrations);
478
+ for callback in ready_callbacks {
479
+ callback();
480
+ }
481
+ });
482
+ }
483
+
484
+ #[cfg(test)]
485
+ mod tests {
486
+ use super::{notify_font_loaded, FontFace, FontFamily, FontStack, FontStyle, FontWeight};
487
+ use crate::assets;
488
+ use std::cell::Cell;
489
+ use std::rc::Rc;
490
+
491
+ #[test]
492
+ fn font_face_on_loaded_waits_for_bridge_callback() {
493
+ assets::test_reset();
494
+ let fired = Rc::new(Cell::new(0));
495
+ FontFace::new(1024).on_loaded({
496
+ let fired = fired.clone();
497
+ move |_| fired.set(fired.get() + 1)
498
+ });
499
+ assert_eq!(fired.get(), 0);
500
+ assets::on_font_loaded(1024);
501
+ notify_font_loaded(1024);
502
+ assert_eq!(fired.get(), 1);
503
+ }
504
+
505
+ #[test]
506
+ fn font_stack_reports_required_font_ids() {
507
+ let stack = FontStack::from_id(1)
508
+ .fallback_face(FontFace::new(3))
509
+ .fallback_stack(FontStack::from_id(4));
510
+ assert_eq!(stack.required_font_ids(), vec![1, 3, 4]);
511
+ }
512
+
513
+ #[test]
514
+ fn font_family_resolves_best_matching_face() {
515
+ let family = FontFamily::from_ids(1, 2, 5, 6, 9, 10, 11, 12);
516
+ assert_eq!(family.resolve(FontWeight::Bold, FontStyle::Italic), 6);
517
+ assert_eq!(family.resolve(FontWeight::Semibold, FontStyle::Normal), 11);
518
+ assert_eq!(family.resolve(FontWeight::Medium, FontStyle::Italic), 10);
519
+ }
520
+ }
@@ -0,0 +1,108 @@
1
+ use crate::bindings::ui;
2
+ use crate::signal::{Callback, Signal, SubscriptionGuard};
3
+ use std::cell::RefCell;
4
+ use std::rc::Rc;
5
+
6
+ #[derive(Clone, Copy)]
7
+ enum ViewportAxis {
8
+ Width,
9
+ Height,
10
+ }
11
+
12
+ #[derive(Clone, Copy)]
13
+ pub struct ViewportSignalHandle {
14
+ axis: ViewportAxis,
15
+ }
16
+
17
+ thread_local! {
18
+ static VIEWPORT_WIDTH_SIGNAL: RefCell<Signal<f32>> = RefCell::new(Signal::new(ui::get_viewport_width()));
19
+ static VIEWPORT_HEIGHT_SIGNAL: RefCell<Signal<f32>> = RefCell::new(Signal::new(ui::get_viewport_height()));
20
+ }
21
+
22
+ fn update_signal(signal: &RefCell<Signal<f32>>, next: f32) {
23
+ let callbacks = signal.borrow_mut().set(next);
24
+ if let Some(callbacks) = callbacks {
25
+ for callback in callbacks {
26
+ callback();
27
+ }
28
+ }
29
+ }
30
+
31
+ impl ViewportSignalHandle {
32
+ pub fn value(&self) -> f32 {
33
+ match self.axis {
34
+ ViewportAxis::Width => VIEWPORT_WIDTH_SIGNAL.with(|slot| slot.borrow().get()),
35
+ ViewportAxis::Height => VIEWPORT_HEIGHT_SIGNAL.with(|slot| slot.borrow().get()),
36
+ }
37
+ }
38
+
39
+ pub fn subscribe(&self, handler: impl Fn(f32) + 'static) -> SubscriptionGuard {
40
+ handler(self.value());
41
+ match self.axis {
42
+ ViewportAxis::Width => VIEWPORT_WIDTH_SIGNAL.with(|slot| {
43
+ let callback: Callback = Rc::new(move || handler(viewport_width_signal().value()));
44
+ slot.borrow_mut().subscribe(callback)
45
+ }),
46
+ ViewportAxis::Height => VIEWPORT_HEIGHT_SIGNAL.with(|slot| {
47
+ let callback: Callback = Rc::new(move || handler(viewport_height_signal().value()));
48
+ slot.borrow_mut().subscribe(callback)
49
+ }),
50
+ }
51
+ }
52
+ }
53
+
54
+ pub fn viewport_width_signal() -> ViewportSignalHandle {
55
+ ViewportSignalHandle {
56
+ axis: ViewportAxis::Width,
57
+ }
58
+ }
59
+
60
+ pub fn viewport_height_signal() -> ViewportSignalHandle {
61
+ ViewportSignalHandle {
62
+ axis: ViewportAxis::Height,
63
+ }
64
+ }
65
+
66
+ pub(crate) fn set_viewport_size(width: f32, height: f32) {
67
+ VIEWPORT_WIDTH_SIGNAL.with(|slot| update_signal(slot, width));
68
+ VIEWPORT_HEIGHT_SIGNAL.with(|slot| update_signal(slot, height));
69
+ }
70
+
71
+ #[cfg(test)]
72
+ mod tests {
73
+ use super::{set_viewport_size, viewport_height_signal, viewport_width_signal};
74
+ use std::cell::Cell;
75
+ use std::rc::Rc;
76
+
77
+ #[test]
78
+ fn viewport_signals_update_and_notify() {
79
+ let width_values = Rc::new(Cell::new(0));
80
+ let height_values = Rc::new(Cell::new(0));
81
+
82
+ let width_seen = Rc::new(Cell::new(0.0));
83
+ let height_seen = Rc::new(Cell::new(0.0));
84
+
85
+ let width_values_clone = width_values.clone();
86
+ let width_seen_clone = width_seen.clone();
87
+ let _width_guard = viewport_width_signal().subscribe(move |value| {
88
+ width_values_clone.set(width_values_clone.get() + 1);
89
+ width_seen_clone.set(value);
90
+ });
91
+
92
+ let height_values_clone = height_values.clone();
93
+ let height_seen_clone = height_seen.clone();
94
+ let _height_guard = viewport_height_signal().subscribe(move |value| {
95
+ height_values_clone.set(height_values_clone.get() + 1);
96
+ height_seen_clone.set(value);
97
+ });
98
+
99
+ set_viewport_size(777.0, 555.0);
100
+
101
+ assert_eq!(viewport_width_signal().value(), 777.0);
102
+ assert_eq!(viewport_height_signal().value(), 555.0);
103
+ assert_eq!(width_seen.get(), 777.0);
104
+ assert_eq!(height_seen.get(), 555.0);
105
+ assert!(width_values.get() >= 2);
106
+ assert!(height_values.get() >= 2);
107
+ }
108
+ }