@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/text.rs ADDED
@@ -0,0 +1,1065 @@
1
+ use crate::assets;
2
+ use crate::bindings::ui;
3
+ use crate::ffi::{TextAlign, TextOverflow, TextVerticalAlign, Unit};
4
+ use crate::frame_scheduler::on_loaded;
5
+ use crate::logger::error;
6
+ use crate::node::{Node, TextNode};
7
+ use crate::theme;
8
+ use crate::typography::{FontFamily, FontStack, FontStyle, FontWeight};
9
+ use std::cell::RefCell;
10
+ use std::ops::Deref;
11
+ use std::rc::{Rc, Weak};
12
+
13
+ const STYLE_RUN_WORD_STRIDE: usize = 7;
14
+
15
+ thread_local! {
16
+ static PENDING_FONT_LAYOUTS: RefCell<Vec<Weak<RefCell<TextLayoutState>>>> = const { RefCell::new(Vec::new()) };
17
+ }
18
+
19
+ #[derive(Clone, Copy, Debug, Default, PartialEq)]
20
+ pub struct TextMetrics {
21
+ pub width: f32,
22
+ pub height: f32,
23
+ pub baseline: f32,
24
+ pub line_count: u32,
25
+ pub max_line_width: f32,
26
+ }
27
+
28
+ #[derive(Clone)]
29
+ pub struct TextLayoutReadyEventArgs {
30
+ pub layout: TextLayout,
31
+ }
32
+
33
+ #[derive(Clone, Debug, PartialEq)]
34
+ pub struct RichTextSpan {
35
+ text: String,
36
+ font_family: Option<FontFamily>,
37
+ font_size: Option<f32>,
38
+ font_weight: Option<FontWeight>,
39
+ font_style: Option<FontStyle>,
40
+ color: Option<u32>,
41
+ background_color: Option<u32>,
42
+ decoration_flags: u32,
43
+ }
44
+
45
+ impl RichTextSpan {
46
+ pub fn new(text: impl Into<String>) -> Self {
47
+ Self {
48
+ text: text.into(),
49
+ font_family: None,
50
+ font_size: None,
51
+ font_weight: None,
52
+ font_style: None,
53
+ color: None,
54
+ background_color: None,
55
+ decoration_flags: 0,
56
+ }
57
+ }
58
+
59
+ pub fn font_stack(mut self, stack: FontStack, size: f32) -> Self {
60
+ self.font_family = Some(FontFamily::with_regular_stack(stack));
61
+ self.font_size = Some(size);
62
+ self
63
+ }
64
+
65
+ pub fn font_family(mut self, family: FontFamily) -> Self {
66
+ self.font_family = Some(family);
67
+ self
68
+ }
69
+
70
+ pub fn font_size(mut self, size: f32) -> Self {
71
+ self.font_size = Some(size);
72
+ self
73
+ }
74
+
75
+ pub fn font_weight(mut self, weight: FontWeight) -> Self {
76
+ self.font_weight = Some(weight);
77
+ self
78
+ }
79
+
80
+ pub fn font_style(mut self, style: FontStyle) -> Self {
81
+ self.font_style = Some(style);
82
+ self
83
+ }
84
+
85
+ pub fn bold(self) -> Self {
86
+ self.font_weight(FontWeight::Bold)
87
+ }
88
+
89
+ pub fn italic(self) -> Self {
90
+ self.font_style(FontStyle::Italic)
91
+ }
92
+
93
+ pub fn text_color(mut self, color: u32) -> Self {
94
+ self.color = Some(color);
95
+ self
96
+ }
97
+
98
+ pub fn background_color(mut self, color: u32) -> Self {
99
+ self.background_color = Some(color);
100
+ self
101
+ }
102
+
103
+ pub fn underline(mut self) -> Self {
104
+ self.decoration_flags |= 1;
105
+ self
106
+ }
107
+
108
+ pub fn strikethrough(mut self) -> Self {
109
+ self.decoration_flags |= 2;
110
+ self
111
+ }
112
+ }
113
+
114
+ pub fn span(text: impl Into<String>) -> RichTextSpan {
115
+ RichTextSpan::new(text)
116
+ }
117
+
118
+ #[derive(Clone)]
119
+ pub struct RichText {
120
+ node: TextNode,
121
+ state: Rc<RefCell<RichTextState>>,
122
+ }
123
+
124
+ #[derive(Clone, Debug, PartialEq)]
125
+ struct RichTextState {
126
+ fragments: Vec<RichTextSpan>,
127
+ base_font_family: Option<FontFamily>,
128
+ has_base_font_value: bool,
129
+ base_font_size: Option<f32>,
130
+ base_font_weight: Option<FontWeight>,
131
+ base_font_style: Option<FontStyle>,
132
+ base_color: Option<u32>,
133
+ }
134
+
135
+ impl RichText {
136
+ pub fn new(fragments: Vec<RichTextSpan>) -> Self {
137
+ let rich_text = Self {
138
+ node: TextNode::new(""),
139
+ state: Rc::new(RefCell::new(RichTextState {
140
+ fragments: Vec::new(),
141
+ base_font_family: None,
142
+ has_base_font_value: false,
143
+ base_font_size: None,
144
+ base_font_weight: None,
145
+ base_font_style: None,
146
+ base_color: None,
147
+ })),
148
+ };
149
+ rich_text.fragments_value(fragments);
150
+ rich_text
151
+ }
152
+
153
+ pub fn from_text(text: impl Into<String>) -> Self {
154
+ Self::new(vec![span(text)])
155
+ }
156
+
157
+ pub fn fragments_value(&self, fragments: Vec<RichTextSpan>) -> &Self {
158
+ self.state.borrow_mut().fragments = fragments;
159
+ self.rebuild_attributed_text();
160
+ self
161
+ }
162
+
163
+ pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
164
+ let mut state = self.state.borrow_mut();
165
+ state.has_base_font_value = true;
166
+ state.base_font_family = Some(FontFamily::with_regular_stack(stack));
167
+ state.base_font_size = Some(size);
168
+ drop(state);
169
+ self.rebuild_attributed_text();
170
+ self
171
+ }
172
+
173
+ pub fn font_family(&self, family: FontFamily) -> &Self {
174
+ let mut state = self.state.borrow_mut();
175
+ state.has_base_font_value = true;
176
+ state.base_font_family = Some(family);
177
+ drop(state);
178
+ self.rebuild_attributed_text();
179
+ self
180
+ }
181
+
182
+ pub fn font_weight(&self, weight: FontWeight) -> &Self {
183
+ let mut state = self.state.borrow_mut();
184
+ state.has_base_font_value = true;
185
+ state.base_font_weight = Some(weight);
186
+ drop(state);
187
+ self.rebuild_attributed_text();
188
+ self
189
+ }
190
+
191
+ pub fn font_style(&self, style: FontStyle) -> &Self {
192
+ let mut state = self.state.borrow_mut();
193
+ state.has_base_font_value = true;
194
+ state.base_font_style = Some(style);
195
+ drop(state);
196
+ self.rebuild_attributed_text();
197
+ self
198
+ }
199
+
200
+ pub fn font_size(&self, size: f32) -> &Self {
201
+ let mut state = self.state.borrow_mut();
202
+ state.has_base_font_value = true;
203
+ state.base_font_size = Some(size);
204
+ drop(state);
205
+ self.rebuild_attributed_text();
206
+ self
207
+ }
208
+
209
+ pub fn text_color(&self, color: u32) -> &Self {
210
+ self.state.borrow_mut().base_color = Some(color);
211
+ self.rebuild_attributed_text();
212
+ self
213
+ }
214
+
215
+ pub fn push(&self, fragment: RichTextSpan) -> &Self {
216
+ self.state.borrow_mut().fragments.push(fragment);
217
+ self.rebuild_attributed_text();
218
+ self
219
+ }
220
+
221
+ pub fn text(&self, content: impl Into<String>) -> &Self {
222
+ self.fragments_value(vec![span(content)])
223
+ }
224
+
225
+ fn rebuild_attributed_text(&self) {
226
+ let compiled = self.compile();
227
+ if compiled.apply_base_font {
228
+ self.node
229
+ .font_id(compiled.base_font_id, compiled.base_font_size);
230
+ }
231
+ self.node.text_color(compiled.base_color);
232
+ self.node.text(compiled.content);
233
+ self.node.style_runs(compiled.runs);
234
+ }
235
+
236
+ fn compile(&self) -> CompiledRichText {
237
+ let state = self.state.borrow();
238
+ compile_rich_text_state(&state)
239
+ }
240
+ }
241
+
242
+ impl Deref for RichText {
243
+ type Target = TextNode;
244
+
245
+ fn deref(&self) -> &Self::Target {
246
+ &self.node
247
+ }
248
+ }
249
+
250
+ impl Node for RichText {
251
+ fn retained_node_ref(&self) -> crate::node::NodeRef {
252
+ self.node.retained_node_ref()
253
+ }
254
+
255
+ fn build_self(&self) {
256
+ self.node.build_self();
257
+ }
258
+ }
259
+
260
+ fn compile_rich_text_state(state: &RichTextState) -> CompiledRichText {
261
+ let mut content = String::new();
262
+ let mut runs = Vec::with_capacity(state.fragments.len() * STYLE_RUN_WORD_STRIDE);
263
+ let theme = theme::current_theme();
264
+ let default_family = state
265
+ .base_font_family
266
+ .clone()
267
+ .unwrap_or_else(|| theme.fonts.body_family.clone());
268
+ let default_weight = state.base_font_weight.unwrap_or(FontWeight::Regular);
269
+ let default_style = state.base_font_style.unwrap_or(FontStyle::Normal);
270
+ let default_size = state.base_font_size.unwrap_or(theme.fonts.size_body);
271
+ let base_font_id = default_family.resolve(default_weight, default_style);
272
+ let base_color = state.base_color.unwrap_or(theme.colors.text_primary);
273
+ let mut start = 0u32;
274
+ for fragment in &state.fragments {
275
+ content.push_str(&fragment.text);
276
+ let end = start + fragment.text.len() as u32;
277
+ let family = fragment
278
+ .font_family
279
+ .clone()
280
+ .unwrap_or_else(|| default_family.clone());
281
+ let weight = fragment.font_weight.unwrap_or(default_weight);
282
+ let style = fragment.font_style.unwrap_or(default_style);
283
+ runs.push(start);
284
+ runs.push(end);
285
+ runs.push(family.resolve(weight, style));
286
+ runs.push(fragment.font_size.unwrap_or(default_size).to_bits());
287
+ runs.push(fragment.color.unwrap_or(base_color));
288
+ runs.push(fragment.background_color.unwrap_or(0));
289
+ runs.push(fragment.decoration_flags);
290
+ start = end;
291
+ }
292
+ CompiledRichText {
293
+ content,
294
+ apply_base_font: state.has_base_font_value || !state.fragments.is_empty(),
295
+ base_font_id,
296
+ base_font_size: default_size,
297
+ base_color,
298
+ runs,
299
+ }
300
+ }
301
+
302
+ struct CompiledRichText {
303
+ content: String,
304
+ apply_base_font: bool,
305
+ base_font_id: u32,
306
+ base_font_size: f32,
307
+ base_color: u32,
308
+ runs: Vec<u32>,
309
+ }
310
+
311
+ type ReadyCallback = Rc<dyn Fn(TextLayoutReadyEventArgs)>;
312
+
313
+ struct TextLayoutState {
314
+ node: TextNode,
315
+ ready: bool,
316
+ metrics: TextMetrics,
317
+ dynamic_charset: Option<String>,
318
+ ready_callbacks: Vec<ReadyCallback>,
319
+ loaded_callback_registered: bool,
320
+ waiting_for_fonts: bool,
321
+ }
322
+
323
+ #[derive(Clone)]
324
+ pub struct TextLayout {
325
+ inner: Rc<RefCell<TextLayoutState>>,
326
+ }
327
+
328
+ impl TextLayout {
329
+ pub fn text(text: impl Into<String>) -> Self {
330
+ Self::from_node(TextNode::new(text.into()))
331
+ }
332
+
333
+ pub fn rich(fragments: Vec<RichTextSpan>) -> Self {
334
+ let rich_text = RichText::new(fragments);
335
+ Self::from_node(rich_text.node.clone())
336
+ }
337
+
338
+ fn from_node(node: TextNode) -> Self {
339
+ Self {
340
+ inner: Rc::new(RefCell::new(TextLayoutState {
341
+ node,
342
+ ready: false,
343
+ metrics: TextMetrics::default(),
344
+ dynamic_charset: None,
345
+ ready_callbacks: Vec::new(),
346
+ loaded_callback_registered: false,
347
+ waiting_for_fonts: false,
348
+ })),
349
+ }
350
+ }
351
+
352
+ pub fn is_ready(&self) -> bool {
353
+ self.inner.borrow().ready
354
+ }
355
+
356
+ pub fn draw_node(&self) -> TextNode {
357
+ self.ensure_built();
358
+ self.inner.borrow().node.clone()
359
+ }
360
+
361
+ pub fn measure(&self) -> TextMetrics {
362
+ if !self.is_ready() {
363
+ error(
364
+ "TextLayout",
365
+ "TextLayout.measure() called before the TextLayout was ready; register on_ready and measure after the callback.",
366
+ );
367
+ return TextMetrics::default();
368
+ }
369
+ self.inner.borrow().metrics
370
+ }
371
+
372
+ pub fn measured_width(&self) -> f32 {
373
+ self.measure().width
374
+ }
375
+
376
+ pub fn measured_height(&self) -> f32 {
377
+ self.measure().height
378
+ }
379
+
380
+ pub fn on_ready(&self, callback: impl Fn(TextLayoutReadyEventArgs) + 'static) -> &Self {
381
+ if self.is_ready() {
382
+ callback(TextLayoutReadyEventArgs {
383
+ layout: self.clone(),
384
+ });
385
+ return self;
386
+ }
387
+ self.inner
388
+ .borrow_mut()
389
+ .ready_callbacks
390
+ .push(Rc::new(callback) as ReadyCallback);
391
+ self.schedule_ready();
392
+ self
393
+ }
394
+
395
+ pub fn set_text(&self, value: impl Into<String>) -> &Self {
396
+ self.inner.borrow().node.text(value.into());
397
+ self.mark_dirty();
398
+ self
399
+ }
400
+
401
+ pub fn width(&self, value: f32, unit: Unit) -> &Self {
402
+ self.inner.borrow().node.width(value, unit);
403
+ self.mark_dirty();
404
+ self
405
+ }
406
+
407
+ pub fn height(&self, value: f32, unit: Unit) -> &Self {
408
+ self.inner.borrow().node.height(value, unit);
409
+ self.mark_dirty();
410
+ self
411
+ }
412
+
413
+ pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
414
+ self.inner.borrow().node.font_stack(stack, size);
415
+ self.mark_dirty();
416
+ self
417
+ }
418
+
419
+ pub fn font_family(&self, family: FontFamily) -> &Self {
420
+ self.inner.borrow().node.font_family(family);
421
+ self.mark_dirty();
422
+ self
423
+ }
424
+
425
+ pub fn font_weight(&self, weight: FontWeight) -> &Self {
426
+ self.inner.borrow().node.font_weight(weight);
427
+ self.mark_dirty();
428
+ self
429
+ }
430
+
431
+ pub fn font_style(&self, style: FontStyle) -> &Self {
432
+ self.inner.borrow().node.font_style(style);
433
+ self.mark_dirty();
434
+ self
435
+ }
436
+
437
+ pub fn font_size(&self, size: f32) -> &Self {
438
+ self.inner.borrow().node.font_size(size);
439
+ self.mark_dirty();
440
+ self
441
+ }
442
+
443
+ pub fn line_height(&self, line_height: f32) -> &Self {
444
+ self.inner.borrow().node.line_height(line_height);
445
+ self.mark_dirty();
446
+ self
447
+ }
448
+
449
+ pub fn text_color(&self, color: u32) -> &Self {
450
+ self.inner.borrow().node.text_color(color);
451
+ self.mark_dirty();
452
+ self
453
+ }
454
+
455
+ pub fn text_align(&self, align: TextAlign) -> &Self {
456
+ self.inner.borrow().node.text_align(align);
457
+ self.mark_dirty();
458
+ self
459
+ }
460
+
461
+ pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
462
+ self.inner.borrow().node.text_vertical_align(align);
463
+ self.mark_dirty();
464
+ self
465
+ }
466
+
467
+ pub fn text_limits(&self, max_chars: i32, max_lines: i32) -> &Self {
468
+ self.inner.borrow().node.text_limits(max_chars, max_lines);
469
+ self.mark_dirty();
470
+ self
471
+ }
472
+
473
+ pub fn max_lines(&self, max_lines: i32) -> &Self {
474
+ self.text_limits(i32::MAX, max_lines)
475
+ }
476
+
477
+ pub fn wrapping(&self, wrap: bool) -> &Self {
478
+ self.inner.borrow().node.wrapping(wrap);
479
+ self.mark_dirty();
480
+ self
481
+ }
482
+
483
+ pub fn wrap(&self, wrap: bool) -> &Self {
484
+ self.wrapping(wrap)
485
+ }
486
+
487
+ pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
488
+ self.inner.borrow().node.text_overflow(overflow);
489
+ self.mark_dirty();
490
+ self
491
+ }
492
+
493
+ pub fn overflow(&self, overflow: TextOverflow) -> &Self {
494
+ self.text_overflow(overflow)
495
+ }
496
+
497
+ pub fn text_overflow_fade(&self, horizontal: bool, vertical: bool) -> &Self {
498
+ self.inner
499
+ .borrow()
500
+ .node
501
+ .text_overflow_fade(horizontal, vertical);
502
+ self.mark_dirty();
503
+ self
504
+ }
505
+
506
+ fn set_dynamic_charset_internal(&self, charset: String) {
507
+ self.inner.borrow_mut().dynamic_charset = Some(charset);
508
+ self.mark_dirty();
509
+ }
510
+
511
+ fn ensure_built(&self) {
512
+ let node = self.inner.borrow().node.clone();
513
+ node.build();
514
+ }
515
+
516
+ fn required_font_ids(&self) -> Vec<u32> {
517
+ self.inner.borrow().node.required_font_ids()
518
+ }
519
+
520
+ fn fonts_ready(&self) -> bool {
521
+ let required = self.required_font_ids();
522
+ if required.is_empty() {
523
+ return true;
524
+ }
525
+ required.into_iter().all(assets::is_font_loaded)
526
+ }
527
+
528
+ fn mark_dirty(&self) {
529
+ let mut state = self.inner.borrow_mut();
530
+ state.ready = false;
531
+ state.metrics = TextMetrics::default();
532
+ state.waiting_for_fonts = false;
533
+ }
534
+
535
+ fn schedule_ready(&self) {
536
+ let already_registered = {
537
+ let mut state = self.inner.borrow_mut();
538
+ if state.loaded_callback_registered {
539
+ true
540
+ } else {
541
+ state.loaded_callback_registered = true;
542
+ false
543
+ }
544
+ };
545
+ if already_registered {
546
+ return;
547
+ }
548
+ let layout = self.clone();
549
+ on_loaded(move |_| {
550
+ layout.inner.borrow_mut().loaded_callback_registered = false;
551
+ layout.prepare_or_wait();
552
+ });
553
+ }
554
+
555
+ fn prepare_or_wait(&self) {
556
+ self.ensure_built();
557
+ if !self.fonts_ready() {
558
+ self.register_waiting_for_fonts();
559
+ return;
560
+ }
561
+ self.prepare_now();
562
+ }
563
+
564
+ fn register_waiting_for_fonts(&self) {
565
+ let already_waiting = {
566
+ let mut state = self.inner.borrow_mut();
567
+ if state.waiting_for_fonts {
568
+ true
569
+ } else {
570
+ state.waiting_for_fonts = true;
571
+ false
572
+ }
573
+ };
574
+ if already_waiting {
575
+ return;
576
+ }
577
+ let weak = Rc::downgrade(&self.inner);
578
+ PENDING_FONT_LAYOUTS.with(|layouts| layouts.borrow_mut().push(weak));
579
+ }
580
+
581
+ fn prepare_now(&self) {
582
+ self.ensure_built();
583
+ let (node, dynamic_charset) = {
584
+ let state = self.inner.borrow();
585
+ (state.node.clone(), state.dynamic_charset.clone())
586
+ };
587
+ let handle = node.handle().raw();
588
+ if let Some(charset) = dynamic_charset {
589
+ ui::set_dynamic_text_charset(handle, &charset);
590
+ }
591
+ if ui::prepare_node(handle) == 0 {
592
+ let mut state = self.inner.borrow_mut();
593
+ state.ready = false;
594
+ state.metrics = TextMetrics::default();
595
+ return;
596
+ }
597
+ let metrics = ui::get_text_metrics(handle).unwrap_or([0.0, 0.0, 0.0, 0.0, 0.0]);
598
+ let callbacks = {
599
+ let mut state = self.inner.borrow_mut();
600
+ state.ready = true;
601
+ state.metrics = TextMetrics {
602
+ width: metrics[0],
603
+ height: metrics[1],
604
+ baseline: metrics[2],
605
+ line_count: metrics[3] as u32,
606
+ max_line_width: metrics[4],
607
+ };
608
+ state.waiting_for_fonts = false;
609
+ std::mem::take(&mut state.ready_callbacks)
610
+ };
611
+ for callback in callbacks {
612
+ callback(TextLayoutReadyEventArgs {
613
+ layout: self.clone(),
614
+ });
615
+ }
616
+ }
617
+ }
618
+
619
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
620
+ pub enum DynamicTextOverflow {
621
+ Reject = 0,
622
+ FallbackShape = 1,
623
+ }
624
+
625
+ struct DynamicTextLayoutState {
626
+ layout: TextLayout,
627
+ charset: String,
628
+ overflow: DynamicTextOverflow,
629
+ current_text: String,
630
+ numeric_mode: bool,
631
+ numeric_precision: i32,
632
+ numeric_prefix: String,
633
+ numeric_suffix: String,
634
+ has_numeric_value: bool,
635
+ numeric_value: f64,
636
+ }
637
+
638
+ #[derive(Clone)]
639
+ pub struct DynamicTextLayout {
640
+ inner: Rc<RefCell<DynamicTextLayoutState>>,
641
+ }
642
+
643
+ impl DynamicTextLayout {
644
+ pub fn fixed_charset(charset: impl Into<String>) -> Self {
645
+ let layout = TextLayout::text("");
646
+ let charset = charset.into();
647
+ layout.set_dynamic_charset_internal(charset.clone());
648
+ Self {
649
+ inner: Rc::new(RefCell::new(DynamicTextLayoutState {
650
+ layout,
651
+ charset,
652
+ overflow: DynamicTextOverflow::FallbackShape,
653
+ current_text: String::new(),
654
+ numeric_mode: false,
655
+ numeric_precision: -1,
656
+ numeric_prefix: String::new(),
657
+ numeric_suffix: String::new(),
658
+ has_numeric_value: false,
659
+ numeric_value: 0.0,
660
+ })),
661
+ }
662
+ }
663
+
664
+ pub fn numeric() -> Self {
665
+ let layout = Self::fixed_charset("0123456789.-");
666
+ layout.inner.borrow_mut().numeric_mode = true;
667
+ layout
668
+ }
669
+
670
+ pub fn is_ready(&self) -> bool {
671
+ self.inner.borrow().layout.is_ready()
672
+ }
673
+
674
+ pub fn measure(&self) -> TextMetrics {
675
+ self.inner.borrow().layout.measure()
676
+ }
677
+
678
+ pub fn current_text(&self) -> String {
679
+ self.inner.borrow().current_text.clone()
680
+ }
681
+
682
+ pub fn on_ready(&self, callback: impl Fn(TextLayoutReadyEventArgs) + 'static) -> &Self {
683
+ self.inner.borrow().layout.on_ready(callback);
684
+ self
685
+ }
686
+
687
+ pub fn width(&self, value: f32, unit: Unit) -> &Self {
688
+ self.inner.borrow().layout.width(value, unit);
689
+ self
690
+ }
691
+
692
+ pub fn height(&self, value: f32, unit: Unit) -> &Self {
693
+ self.inner.borrow().layout.height(value, unit);
694
+ self
695
+ }
696
+
697
+ pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
698
+ self.inner.borrow().layout.font_stack(stack, size);
699
+ self
700
+ }
701
+
702
+ pub fn font_family(&self, family: FontFamily) -> &Self {
703
+ self.inner.borrow().layout.font_family(family);
704
+ self
705
+ }
706
+
707
+ pub fn font_weight(&self, weight: FontWeight) -> &Self {
708
+ self.inner.borrow().layout.font_weight(weight);
709
+ self
710
+ }
711
+
712
+ pub fn font_style(&self, style: FontStyle) -> &Self {
713
+ self.inner.borrow().layout.font_style(style);
714
+ self
715
+ }
716
+
717
+ pub fn font_size(&self, size: f32) -> &Self {
718
+ self.inner.borrow().layout.font_size(size);
719
+ self
720
+ }
721
+
722
+ pub fn line_height(&self, line_height: f32) -> &Self {
723
+ self.inner.borrow().layout.line_height(line_height);
724
+ self
725
+ }
726
+
727
+ pub fn text_color(&self, color: u32) -> &Self {
728
+ self.inner.borrow().layout.text_color(color);
729
+ self
730
+ }
731
+
732
+ pub fn text_align(&self, align: TextAlign) -> &Self {
733
+ self.inner.borrow().layout.text_align(align);
734
+ self
735
+ }
736
+
737
+ pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
738
+ self.inner.borrow().layout.text_vertical_align(align);
739
+ self
740
+ }
741
+
742
+ pub fn text_limits(&self, max_chars: i32, max_lines: i32) -> &Self {
743
+ self.inner.borrow().layout.text_limits(max_chars, max_lines);
744
+ self
745
+ }
746
+
747
+ pub fn max_lines(&self, max_lines: i32) -> &Self {
748
+ self.inner.borrow().layout.max_lines(max_lines);
749
+ self
750
+ }
751
+
752
+ pub fn wrap(&self, wrap: bool) -> &Self {
753
+ self.inner.borrow().layout.wrap(wrap);
754
+ self
755
+ }
756
+
757
+ pub fn wrapping(&self, wrap: bool) -> &Self {
758
+ self.wrap(wrap)
759
+ }
760
+
761
+ pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
762
+ self.inner.borrow().layout.text_overflow(overflow);
763
+ self
764
+ }
765
+
766
+ pub fn overflow(&self, mode: DynamicTextOverflow) -> &Self {
767
+ self.inner.borrow_mut().overflow = mode;
768
+ self
769
+ }
770
+
771
+ pub fn set_text(&self, value: impl Into<String>) -> bool {
772
+ let value = value.into();
773
+ if !self.supports_text(&value)
774
+ && self.inner.borrow().overflow == DynamicTextOverflow::Reject
775
+ {
776
+ return false;
777
+ }
778
+ let layout = self.inner.borrow().layout.clone();
779
+ let was_ready = layout.is_ready();
780
+ self.inner.borrow_mut().current_text = value.clone();
781
+ layout.set_text(value);
782
+ if was_ready {
783
+ layout.prepare_or_wait();
784
+ }
785
+ true
786
+ }
787
+
788
+ pub fn text(&self, value: impl Into<String>) -> &Self {
789
+ let _ = self.set_text(value);
790
+ self
791
+ }
792
+
793
+ pub fn precision(&self, digits: i32) -> &Self {
794
+ let mut state = self.inner.borrow_mut();
795
+ state.numeric_mode = true;
796
+ state.numeric_precision = digits.max(0);
797
+ drop(state);
798
+ self.refresh_numeric_text();
799
+ self
800
+ }
801
+
802
+ pub fn prefix(&self, value: impl Into<String>) -> &Self {
803
+ let value = value.into();
804
+ {
805
+ let mut state = self.inner.borrow_mut();
806
+ state.numeric_mode = true;
807
+ state.numeric_prefix = value.clone();
808
+ Self::include_in_charset(&mut state, &value);
809
+ let layout = state.layout.clone();
810
+ let charset = state.charset.clone();
811
+ drop(state);
812
+ layout.set_dynamic_charset_internal(charset);
813
+ }
814
+ self.refresh_numeric_text();
815
+ self
816
+ }
817
+
818
+ pub fn suffix(&self, value: impl Into<String>) -> &Self {
819
+ let value = value.into();
820
+ {
821
+ let mut state = self.inner.borrow_mut();
822
+ state.numeric_mode = true;
823
+ state.numeric_suffix = value.clone();
824
+ Self::include_in_charset(&mut state, &value);
825
+ let layout = state.layout.clone();
826
+ let charset = state.charset.clone();
827
+ drop(state);
828
+ layout.set_dynamic_charset_internal(charset);
829
+ }
830
+ self.refresh_numeric_text();
831
+ self
832
+ }
833
+
834
+ pub fn set_value(&self, value: f64) -> bool {
835
+ {
836
+ let mut state = self.inner.borrow_mut();
837
+ state.numeric_mode = true;
838
+ state.has_numeric_value = true;
839
+ state.numeric_value = value;
840
+ }
841
+ self.set_text(self.compose_numeric_text(value))
842
+ }
843
+
844
+ pub fn draw_node(&self) -> TextNode {
845
+ self.inner.borrow().layout.draw_node()
846
+ }
847
+
848
+ fn supports_text(&self, value: &str) -> bool {
849
+ let charset = self.inner.borrow().charset.clone();
850
+ if charset.is_empty() {
851
+ return true;
852
+ }
853
+ value.chars().all(|ch| charset.contains(ch))
854
+ }
855
+
856
+ fn include_in_charset(state: &mut DynamicTextLayoutState, value: &str) {
857
+ for ch in value.chars() {
858
+ if !state.charset.contains(ch) {
859
+ state.charset.push(ch);
860
+ }
861
+ }
862
+ }
863
+
864
+ fn refresh_numeric_text(&self) {
865
+ let (numeric_mode, has_value, numeric_value) = {
866
+ let state = self.inner.borrow();
867
+ (
868
+ state.numeric_mode,
869
+ state.has_numeric_value,
870
+ state.numeric_value,
871
+ )
872
+ };
873
+ if !numeric_mode || !has_value {
874
+ return;
875
+ }
876
+ let _ = self.set_text(self.compose_numeric_text(numeric_value));
877
+ }
878
+
879
+ fn compose_numeric_text(&self, value: f64) -> String {
880
+ let mut state = self.inner.borrow_mut();
881
+ let prefix = state.numeric_prefix.clone();
882
+ let suffix = state.numeric_suffix.clone();
883
+ Self::include_in_charset(&mut state, &prefix);
884
+ Self::include_in_charset(&mut state, &suffix);
885
+ let charset = state.charset.clone();
886
+ let layout = state.layout.clone();
887
+ drop(state);
888
+ layout.set_dynamic_charset_internal(charset);
889
+ format!("{prefix}{}{suffix}", self.format_numeric_value(value))
890
+ }
891
+
892
+ fn format_numeric_value(&self, value: f64) -> String {
893
+ let precision = self.inner.borrow().numeric_precision;
894
+ if value.is_nan() || !value.is_finite() || precision < 0 {
895
+ return value.to_string();
896
+ }
897
+ format!("{value:.precision$}", precision = precision as usize)
898
+ }
899
+ }
900
+
901
+ pub(crate) fn notify_font_loaded(_font_id: u32) {
902
+ PENDING_FONT_LAYOUTS.with(|layouts| {
903
+ let mut layouts = layouts.borrow_mut();
904
+ layouts.retain(|weak| {
905
+ let Some(inner) = weak.upgrade() else {
906
+ return false;
907
+ };
908
+ let layout = TextLayout { inner };
909
+ if layout.fonts_ready() {
910
+ layout.prepare_now();
911
+ false
912
+ } else {
913
+ true
914
+ }
915
+ });
916
+ });
917
+ }
918
+
919
+ #[cfg(test)]
920
+ mod tests {
921
+ use super::{
922
+ notify_font_loaded, span, DynamicTextLayout, DynamicTextOverflow, RichText, TextLayout,
923
+ };
924
+ use crate::assets;
925
+ use crate::ffi::{self, Call, TextAlign, TextOverflow, TextVerticalAlign, Unit};
926
+ use crate::frame_scheduler;
927
+ use crate::node::Node;
928
+ use std::cell::RefCell;
929
+ use std::rc::Rc;
930
+
931
+ #[test]
932
+ fn rich_text_emits_style_runs() {
933
+ ffi::test::reset();
934
+ let node = RichText::new(vec![span("Hello")
935
+ .font_stack(crate::typography::FontStack::from_id(7), 18.0)
936
+ .text_color(0xFF00FFFF)]);
937
+ node.build();
938
+ let calls = ffi::test::take_calls();
939
+ assert!(calls
940
+ .iter()
941
+ .any(|call| matches!(call, Call::SetTextStyleRuns { run_count: 1, .. })));
942
+ }
943
+
944
+ #[test]
945
+ fn rich_text_retained_mutations_update_text_and_style_runs() {
946
+ ffi::test::reset();
947
+ let node = RichText::from_text("Before");
948
+ node.build();
949
+ ffi::test::take_calls();
950
+
951
+ node.fragments_value(vec![
952
+ span("After").font_stack(crate::typography::FontStack::from_id(8), 19.0),
953
+ span(" ✅").text_color(0x00FF00FF),
954
+ ]);
955
+
956
+ let calls = ffi::test::take_calls();
957
+ assert!(calls
958
+ .iter()
959
+ .any(|call| matches!(call, Call::SetText { text, .. } if text == "After ✅")));
960
+ assert!(calls
961
+ .iter()
962
+ .any(|call| matches!(call, Call::SetTextStyleRuns { run_count: 2, .. })));
963
+ }
964
+
965
+ #[test]
966
+ fn text_layout_waits_for_loaded_and_fonts_before_reporting_ready() {
967
+ ffi::test::reset();
968
+ frame_scheduler::reset_commit_state();
969
+ ffi::test::set_text_metrics(88.0, 22.0, 16.0, 2, 64.0);
970
+
971
+ let ready_count = Rc::new(RefCell::new(0));
972
+ let layout = TextLayout::text("Layout");
973
+ layout
974
+ .font_stack(crate::typography::FontStack::from_id(99), 16.0)
975
+ .on_ready({
976
+ let ready_count = ready_count.clone();
977
+ move |_| *ready_count.borrow_mut() += 1
978
+ });
979
+
980
+ frame_scheduler::fire_loaded_callbacks();
981
+ assert_eq!(*ready_count.borrow(), 0);
982
+
983
+ assets::on_font_loaded(99);
984
+ notify_font_loaded(99);
985
+ assert_eq!(*ready_count.borrow(), 1);
986
+
987
+ let calls = ffi::test::take_calls();
988
+ assert!(calls
989
+ .iter()
990
+ .any(|call| matches!(call, Call::PrepareNode { .. })));
991
+ assert!(calls
992
+ .iter()
993
+ .any(|call| matches!(call, Call::GetTextMetrics { .. })));
994
+ }
995
+
996
+ #[test]
997
+ fn text_layout_emits_display_configuration() {
998
+ ffi::test::reset();
999
+ frame_scheduler::reset_commit_state();
1000
+ assets::on_font_loaded(1);
1001
+ TextLayout::text("Layout")
1002
+ .line_height(26.0)
1003
+ .text_align(TextAlign::Right)
1004
+ .text_vertical_align(TextVerticalAlign::Center)
1005
+ .text_limits(12, 3)
1006
+ .wrapping(true)
1007
+ .text_overflow(TextOverflow::Ellipsis)
1008
+ .text_overflow_fade(true, false)
1009
+ .on_ready(|_| {});
1010
+ frame_scheduler::fire_loaded_callbacks();
1011
+ let calls = ffi::test::take_calls();
1012
+ assert!(calls.iter().any(|call| matches!(call, Call::SetLineHeight { line_height, .. } if (*line_height - 26.0).abs() < f32::EPSILON)));
1013
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextAlign { align_enum, .. } if *align_enum == TextAlign::Right as u32)));
1014
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextVerticalAlign { align_enum, .. } if *align_enum == TextVerticalAlign::Center as u32)));
1015
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextLimits { max_chars, max_lines, .. } if *max_chars == 12 && *max_lines == 3)));
1016
+ assert!(calls
1017
+ .iter()
1018
+ .any(|call| matches!(call, Call::SetTextWrapping { wrap: true, .. })));
1019
+ assert!(calls.iter().any(|call| matches!(call, Call::SetTextOverflow { overflow_enum, .. } if *overflow_enum == TextOverflow::Ellipsis as u32)));
1020
+ assert!(calls.iter().any(|call| matches!(
1021
+ call,
1022
+ Call::SetTextOverflowFade {
1023
+ horizontal: true,
1024
+ vertical: false,
1025
+ ..
1026
+ }
1027
+ )));
1028
+ }
1029
+
1030
+ #[test]
1031
+ fn dynamic_text_layout_rejects_unsupported_text_when_configured() {
1032
+ ffi::test::reset();
1033
+ let layout = DynamicTextLayout::fixed_charset("0123456789");
1034
+ layout.overflow(DynamicTextOverflow::Reject);
1035
+ assert!(layout.set_text("123"));
1036
+ assert!(!layout.set_text("12a"));
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn dynamic_text_layout_reprepares_after_ready_text_update() {
1041
+ ffi::test::reset();
1042
+ frame_scheduler::reset_commit_state();
1043
+ assets::test_reset();
1044
+
1045
+ let layout = DynamicTextLayout::fixed_charset("0123456789");
1046
+ layout
1047
+ .font_stack(crate::typography::FontStack::from_id(1), 16.0)
1048
+ .width(120.0, Unit::Pixel)
1049
+ .height(24.0, Unit::Pixel)
1050
+ .on_ready(|_| {});
1051
+ frame_scheduler::fire_loaded_callbacks();
1052
+ assert!(layout.is_ready());
1053
+ ffi::test::take_calls();
1054
+
1055
+ assert!(layout.set_text("42"));
1056
+ assert!(layout.is_ready());
1057
+ let calls = ffi::test::take_calls();
1058
+ assert!(calls
1059
+ .iter()
1060
+ .any(|call| matches!(call, Call::PrepareNode { .. })));
1061
+ assert!(calls
1062
+ .iter()
1063
+ .any(|call| matches!(call, Call::SetDynamicTextCharset { charset, .. } if charset == "0123456789")));
1064
+ }
1065
+ }