1use crate::assets;
2use crate::bindings::ui;
3use crate::ffi::{TextAlign, TextOverflow, TextVerticalAlign, Unit};
4use crate::frame_scheduler::on_loaded;
5use crate::logger::error;
6use crate::node::{HasTextNode, Node, TextNode, ThemeBindable};
7use crate::theme;
8use crate::typography::{FontFamily, FontStack, FontStyle, FontWeight};
9use std::cell::RefCell;
10use std::ops::Deref;
11use std::rc::{Rc, Weak};
12
13const STYLE_RUN_WORD_STRIDE: usize = 7;
14
15thread_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)]
20pub 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)]
29pub struct TextLayoutReadyEventArgs {
30 pub layout: TextLayout,
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub 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
45impl 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
114pub fn span(text: impl Into<String>) -> RichTextSpan {
115 RichTextSpan::new(text)
116}
117
118#[derive(Clone)]
119pub struct RichText {
120 node: TextNode,
121 state: Rc<RefCell<RichTextState>>,
122}
123
124#[derive(Clone, Debug, PartialEq)]
125struct 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
135impl 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
242impl Deref for RichText {
243 type Target = TextNode;
244
245 fn deref(&self) -> &Self::Target {
246 &self.node
247 }
248}
249
250impl HasTextNode for RichText {
251 fn text_node(&self) -> &TextNode {
252 &self.node
253 }
254
255 fn set_text_surface_content(&self, content: String) {
256 self.text(content);
257 }
258
259 fn set_text_surface_font_stack(&self, stack: FontStack, size: f32) {
260 self.font_stack(stack, size);
261 }
262
263 fn set_text_surface_font_family(&self, family: FontFamily) {
264 self.font_family(family);
265 }
266
267 fn set_text_surface_font_weight(&self, weight: FontWeight) {
268 self.font_weight(weight);
269 }
270
271 fn set_text_surface_font_style(&self, style: FontStyle) {
272 self.font_style(style);
273 }
274
275 fn set_text_surface_font_size(&self, size: f32) {
276 self.font_size(size);
277 }
278
279 fn set_text_surface_color(&self, color: u32) {
280 self.text_color(color);
281 }
282}
283
284impl ThemeBindable for RichText {
285 fn theme_binding_node(&self) -> crate::node::NodeRef {
286 self.node.retained_node_ref()
287 }
288
289 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
290 let weak_node = self.node.weak_theme_target();
291 let weak_state = Rc::downgrade(&self.state);
292 Box::new(move || {
293 Some(Self {
294 node: weak_node()?,
295 state: weak_state.upgrade()?,
296 })
297 })
298 }
299}
300
301impl Node for RichText {
302 fn retained_node_ref(&self) -> crate::node::NodeRef {
303 self.node.retained_node_ref()
304 }
305
306 fn build_self(&self) {
307 self.node.build_self();
308 }
309
310 fn required_font_ids_for_preparation(&self) -> Vec<u32> {
311 self.node.required_font_ids()
312 }
313}
314
315fn compile_rich_text_state(state: &RichTextState) -> CompiledRichText {
316 let mut content = String::new();
317 let mut runs = Vec::with_capacity(state.fragments.len() * STYLE_RUN_WORD_STRIDE);
318 let theme = theme::current_theme();
319 let default_family = state
320 .base_font_family
321 .clone()
322 .unwrap_or_else(|| theme.fonts.body_family.clone());
323 let default_weight = state.base_font_weight.unwrap_or(FontWeight::Regular);
324 let default_style = state.base_font_style.unwrap_or(FontStyle::Normal);
325 let default_size = state.base_font_size.unwrap_or(theme.fonts.size_body);
326 let base_font_id = default_family.resolve(default_weight, default_style);
327 let base_color = state.base_color.unwrap_or(theme.colors.text_primary);
328 let mut start = 0u32;
329 for fragment in &state.fragments {
330 content.push_str(&fragment.text);
331 let end = start + fragment.text.len() as u32;
332 let family = fragment
333 .font_family
334 .clone()
335 .unwrap_or_else(|| default_family.clone());
336 let weight = fragment.font_weight.unwrap_or(default_weight);
337 let style = fragment.font_style.unwrap_or(default_style);
338 runs.push(start);
339 runs.push(end);
340 runs.push(family.resolve(weight, style));
341 runs.push(fragment.font_size.unwrap_or(default_size).to_bits());
342 runs.push(fragment.color.unwrap_or(base_color));
343 runs.push(fragment.background_color.unwrap_or(0));
344 runs.push(fragment.decoration_flags);
345 start = end;
346 }
347 CompiledRichText {
348 content,
349 apply_base_font: state.has_base_font_value || !state.fragments.is_empty(),
350 base_font_id,
351 base_font_size: default_size,
352 base_color,
353 runs,
354 }
355}
356
357struct CompiledRichText {
358 content: String,
359 apply_base_font: bool,
360 base_font_id: u32,
361 base_font_size: f32,
362 base_color: u32,
363 runs: Vec<u32>,
364}
365
366type ReadyCallback = Rc<dyn Fn(TextLayoutReadyEventArgs)>;
367
368struct TextLayoutState {
369 node: TextNode,
370 ready: bool,
371 metrics: TextMetrics,
372 dynamic_charset: Option<String>,
373 ready_callbacks: Vec<ReadyCallback>,
374 loaded_callback_registered: bool,
375 waiting_for_fonts: bool,
376}
377
378#[derive(Clone)]
379pub struct TextLayout {
380 inner: Rc<RefCell<TextLayoutState>>,
381}
382
383impl TextLayout {
384 pub fn text(text: impl Into<String>) -> Self {
385 Self::from_node(TextNode::new(text.into()))
386 }
387
388 pub fn rich(fragments: Vec<RichTextSpan>) -> Self {
389 let rich_text = RichText::new(fragments);
390 Self::from_node(rich_text.node.clone())
391 }
392
393 fn from_node(node: TextNode) -> Self {
394 Self {
395 inner: Rc::new(RefCell::new(TextLayoutState {
396 node,
397 ready: false,
398 metrics: TextMetrics::default(),
399 dynamic_charset: None,
400 ready_callbacks: Vec::new(),
401 loaded_callback_registered: false,
402 waiting_for_fonts: false,
403 })),
404 }
405 }
406
407 pub fn is_ready(&self) -> bool {
408 self.inner.borrow().ready
409 }
410
411 pub fn draw_node(&self) -> TextNode {
412 self.ensure_built();
413 self.inner.borrow().node.clone()
414 }
415
416 pub fn measure(&self) -> TextMetrics {
417 if !self.is_ready() {
418 error(
419 "TextLayout",
420 "TextLayout.measure() called before the TextLayout was ready; register on_ready and measure after the callback.",
421 );
422 return TextMetrics::default();
423 }
424 self.inner.borrow().metrics
425 }
426
427 pub fn measured_width(&self) -> f32 {
428 self.measure().width
429 }
430
431 pub fn measured_height(&self) -> f32 {
432 self.measure().height
433 }
434
435 pub fn on_ready(&self, callback: impl Fn(TextLayoutReadyEventArgs) + 'static) -> &Self {
436 if self.is_ready() {
437 callback(TextLayoutReadyEventArgs {
438 layout: self.clone(),
439 });
440 return self;
441 }
442 self.inner
443 .borrow_mut()
444 .ready_callbacks
445 .push(Rc::new(callback) as ReadyCallback);
446 self.schedule_ready();
447 self
448 }
449
450 pub fn set_text(&self, value: impl Into<String>) -> &Self {
451 self.inner.borrow().node.text(value.into());
452 self.mark_dirty();
453 self
454 }
455
456 pub fn width(&self, value: f32, unit: Unit) -> &Self {
457 self.inner.borrow().node.width(value, unit);
458 self.mark_dirty();
459 self
460 }
461
462 pub fn height(&self, value: f32, unit: Unit) -> &Self {
463 self.inner.borrow().node.height(value, unit);
464 self.mark_dirty();
465 self
466 }
467
468 pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
469 self.inner.borrow().node.font_stack(stack, size);
470 self.mark_dirty();
471 self
472 }
473
474 pub fn font_family(&self, family: FontFamily) -> &Self {
475 self.inner.borrow().node.font_family(family);
476 self.mark_dirty();
477 self
478 }
479
480 pub fn font_weight(&self, weight: FontWeight) -> &Self {
481 self.inner.borrow().node.font_weight(weight);
482 self.mark_dirty();
483 self
484 }
485
486 pub fn font_style(&self, style: FontStyle) -> &Self {
487 self.inner.borrow().node.font_style(style);
488 self.mark_dirty();
489 self
490 }
491
492 pub fn font_size(&self, size: f32) -> &Self {
493 self.inner.borrow().node.font_size(size);
494 self.mark_dirty();
495 self
496 }
497
498 pub fn line_height(&self, line_height: f32) -> &Self {
499 self.inner.borrow().node.line_height(line_height);
500 self.mark_dirty();
501 self
502 }
503
504 pub fn text_color(&self, color: u32) -> &Self {
505 self.inner.borrow().node.text_color(color);
506 self.mark_dirty();
507 self
508 }
509
510 pub fn text_align(&self, align: TextAlign) -> &Self {
511 self.inner.borrow().node.text_align(align);
512 self.mark_dirty();
513 self
514 }
515
516 pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
517 self.inner.borrow().node.text_vertical_align(align);
518 self.mark_dirty();
519 self
520 }
521
522 pub fn text_limits(&self, max_chars: i32, max_lines: i32) -> &Self {
523 self.inner.borrow().node.text_limits(max_chars, max_lines);
524 self.mark_dirty();
525 self
526 }
527
528 pub fn max_lines(&self, max_lines: i32) -> &Self {
529 self.text_limits(i32::MAX, max_lines)
530 }
531
532 pub fn wrapping(&self, wrap: bool) -> &Self {
533 self.inner.borrow().node.wrapping(wrap);
534 self.mark_dirty();
535 self
536 }
537
538 pub fn wrap(&self, wrap: bool) -> &Self {
539 self.wrapping(wrap)
540 }
541
542 pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
543 self.inner.borrow().node.text_overflow(overflow);
544 self.mark_dirty();
545 self
546 }
547
548 pub fn overflow(&self, overflow: TextOverflow) -> &Self {
549 self.text_overflow(overflow)
550 }
551
552 pub fn text_overflow_fade(&self, horizontal: bool, vertical: bool) -> &Self {
553 self.inner
554 .borrow()
555 .node
556 .text_overflow_fade(horizontal, vertical);
557 self.mark_dirty();
558 self
559 }
560
561 fn set_dynamic_charset_internal(&self, charset: String) {
562 self.inner.borrow_mut().dynamic_charset = Some(charset);
563 self.mark_dirty();
564 }
565
566 fn ensure_built(&self) {
567 let node = self.inner.borrow().node.clone();
568 node.build();
569 }
570
571 fn required_font_ids(&self) -> Vec<u32> {
572 self.inner.borrow().node.required_font_ids()
573 }
574
575 fn fonts_ready(&self) -> bool {
576 let required = self.required_font_ids();
577 if required.is_empty() {
578 return true;
579 }
580 required.into_iter().all(assets::is_font_loaded)
581 }
582
583 fn mark_dirty(&self) {
584 let mut state = self.inner.borrow_mut();
585 state.ready = false;
586 state.metrics = TextMetrics::default();
587 state.waiting_for_fonts = false;
588 }
589
590 fn schedule_ready(&self) {
591 let already_registered = {
592 let mut state = self.inner.borrow_mut();
593 if state.loaded_callback_registered {
594 true
595 } else {
596 state.loaded_callback_registered = true;
597 false
598 }
599 };
600 if already_registered {
601 return;
602 }
603 let layout = self.clone();
604 on_loaded(move |_| {
605 layout.inner.borrow_mut().loaded_callback_registered = false;
606 layout.prepare_or_wait();
607 });
608 }
609
610 fn prepare_or_wait(&self) {
611 self.ensure_built();
612 if !self.fonts_ready() {
613 self.register_waiting_for_fonts();
614 return;
615 }
616 self.prepare_now();
617 }
618
619 fn register_waiting_for_fonts(&self) {
620 let already_waiting = {
621 let mut state = self.inner.borrow_mut();
622 if state.waiting_for_fonts {
623 true
624 } else {
625 state.waiting_for_fonts = true;
626 false
627 }
628 };
629 if already_waiting {
630 return;
631 }
632 let weak = Rc::downgrade(&self.inner);
633 PENDING_FONT_LAYOUTS.with(|layouts| layouts.borrow_mut().push(weak));
634 }
635
636 fn prepare_now(&self) {
637 self.ensure_built();
638 let (node, dynamic_charset) = {
639 let state = self.inner.borrow();
640 (state.node.clone(), state.dynamic_charset.clone())
641 };
642 let handle = node.handle().raw();
643 if let Some(charset) = dynamic_charset {
644 ui::set_dynamic_text_charset(handle, &charset);
645 }
646 if ui::prepare_node(handle) == 0 {
647 let mut state = self.inner.borrow_mut();
648 state.ready = false;
649 state.metrics = TextMetrics::default();
650 return;
651 }
652 let metrics = ui::get_text_metrics(handle).unwrap_or([0.0, 0.0, 0.0, 0.0, 0.0]);
653 let callbacks = {
654 let mut state = self.inner.borrow_mut();
655 state.ready = true;
656 state.metrics = TextMetrics {
657 width: metrics[0],
658 height: metrics[1],
659 baseline: metrics[2],
660 line_count: metrics[3] as u32,
661 max_line_width: metrics[4],
662 };
663 state.waiting_for_fonts = false;
664 std::mem::take(&mut state.ready_callbacks)
665 };
666 for callback in callbacks {
667 callback(TextLayoutReadyEventArgs {
668 layout: self.clone(),
669 });
670 }
671 }
672}
673
674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
675pub enum DynamicTextOverflow {
676 Reject = 0,
677 FallbackShape = 1,
678}
679
680struct DynamicTextLayoutState {
681 layout: TextLayout,
682 charset: String,
683 overflow: DynamicTextOverflow,
684 current_text: String,
685 numeric_mode: bool,
686 numeric_precision: i32,
687 numeric_prefix: String,
688 numeric_suffix: String,
689 has_numeric_value: bool,
690 numeric_value: f64,
691}
692
693#[derive(Clone)]
694pub struct DynamicTextLayout {
695 inner: Rc<RefCell<DynamicTextLayoutState>>,
696}
697
698impl DynamicTextLayout {
699 pub fn fixed_charset(charset: impl Into<String>) -> Self {
700 let layout = TextLayout::text("");
701 let charset = charset.into();
702 layout.set_dynamic_charset_internal(charset.clone());
703 Self {
704 inner: Rc::new(RefCell::new(DynamicTextLayoutState {
705 layout,
706 charset,
707 overflow: DynamicTextOverflow::FallbackShape,
708 current_text: String::new(),
709 numeric_mode: false,
710 numeric_precision: -1,
711 numeric_prefix: String::new(),
712 numeric_suffix: String::new(),
713 has_numeric_value: false,
714 numeric_value: 0.0,
715 })),
716 }
717 }
718
719 pub fn numeric() -> Self {
720 let layout = Self::fixed_charset("0123456789.-");
721 layout.inner.borrow_mut().numeric_mode = true;
722 layout
723 }
724
725 pub fn is_ready(&self) -> bool {
726 self.inner.borrow().layout.is_ready()
727 }
728
729 pub fn measure(&self) -> TextMetrics {
730 self.inner.borrow().layout.measure()
731 }
732
733 pub fn current_text(&self) -> String {
734 self.inner.borrow().current_text.clone()
735 }
736
737 pub fn on_ready(&self, callback: impl Fn(TextLayoutReadyEventArgs) + 'static) -> &Self {
738 self.inner.borrow().layout.on_ready(callback);
739 self
740 }
741
742 pub fn width(&self, value: f32, unit: Unit) -> &Self {
743 self.inner.borrow().layout.width(value, unit);
744 self
745 }
746
747 pub fn height(&self, value: f32, unit: Unit) -> &Self {
748 self.inner.borrow().layout.height(value, unit);
749 self
750 }
751
752 pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
753 self.inner.borrow().layout.font_stack(stack, size);
754 self
755 }
756
757 pub fn font_family(&self, family: FontFamily) -> &Self {
758 self.inner.borrow().layout.font_family(family);
759 self
760 }
761
762 pub fn font_weight(&self, weight: FontWeight) -> &Self {
763 self.inner.borrow().layout.font_weight(weight);
764 self
765 }
766
767 pub fn font_style(&self, style: FontStyle) -> &Self {
768 self.inner.borrow().layout.font_style(style);
769 self
770 }
771
772 pub fn font_size(&self, size: f32) -> &Self {
773 self.inner.borrow().layout.font_size(size);
774 self
775 }
776
777 pub fn line_height(&self, line_height: f32) -> &Self {
778 self.inner.borrow().layout.line_height(line_height);
779 self
780 }
781
782 pub fn text_color(&self, color: u32) -> &Self {
783 self.inner.borrow().layout.text_color(color);
784 self
785 }
786
787 pub fn text_align(&self, align: TextAlign) -> &Self {
788 self.inner.borrow().layout.text_align(align);
789 self
790 }
791
792 pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
793 self.inner.borrow().layout.text_vertical_align(align);
794 self
795 }
796
797 pub fn text_limits(&self, max_chars: i32, max_lines: i32) -> &Self {
798 self.inner.borrow().layout.text_limits(max_chars, max_lines);
799 self
800 }
801
802 pub fn max_lines(&self, max_lines: i32) -> &Self {
803 self.inner.borrow().layout.max_lines(max_lines);
804 self
805 }
806
807 pub fn wrap(&self, wrap: bool) -> &Self {
808 self.inner.borrow().layout.wrap(wrap);
809 self
810 }
811
812 pub fn wrapping(&self, wrap: bool) -> &Self {
813 self.wrap(wrap)
814 }
815
816 pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
817 self.inner.borrow().layout.text_overflow(overflow);
818 self
819 }
820
821 pub fn overflow(&self, mode: DynamicTextOverflow) -> &Self {
822 self.inner.borrow_mut().overflow = mode;
823 self
824 }
825
826 pub fn set_text(&self, value: impl Into<String>) -> bool {
827 let value = value.into();
828 if !self.supports_text(&value)
829 && self.inner.borrow().overflow == DynamicTextOverflow::Reject
830 {
831 return false;
832 }
833 let layout = self.inner.borrow().layout.clone();
834 let was_ready = layout.is_ready();
835 self.inner.borrow_mut().current_text = value.clone();
836 layout.set_text(value);
837 if was_ready {
838 layout.prepare_or_wait();
839 } else {
840 layout.schedule_ready();
841 }
842 true
843 }
844
845 pub fn text(&self, value: impl Into<String>) -> &Self {
846 let _ = self.set_text(value);
847 self
848 }
849
850 pub fn precision(&self, digits: i32) -> &Self {
851 let mut state = self.inner.borrow_mut();
852 state.numeric_mode = true;
853 state.numeric_precision = digits.max(0);
854 drop(state);
855 self.refresh_numeric_text();
856 self
857 }
858
859 pub fn prefix(&self, value: impl Into<String>) -> &Self {
860 let value = value.into();
861 {
862 let mut state = self.inner.borrow_mut();
863 state.numeric_mode = true;
864 state.numeric_prefix = value.clone();
865 Self::include_in_charset(&mut state, &value);
866 let layout = state.layout.clone();
867 let charset = state.charset.clone();
868 drop(state);
869 layout.set_dynamic_charset_internal(charset);
870 }
871 self.refresh_numeric_text();
872 self
873 }
874
875 pub fn suffix(&self, value: impl Into<String>) -> &Self {
876 let value = value.into();
877 {
878 let mut state = self.inner.borrow_mut();
879 state.numeric_mode = true;
880 state.numeric_suffix = value.clone();
881 Self::include_in_charset(&mut state, &value);
882 let layout = state.layout.clone();
883 let charset = state.charset.clone();
884 drop(state);
885 layout.set_dynamic_charset_internal(charset);
886 }
887 self.refresh_numeric_text();
888 self
889 }
890
891 pub fn set_value(&self, value: f64) -> bool {
892 {
893 let mut state = self.inner.borrow_mut();
894 state.numeric_mode = true;
895 state.has_numeric_value = true;
896 state.numeric_value = value;
897 }
898 self.set_text(self.compose_numeric_text(value))
899 }
900
901 pub fn draw_node(&self) -> TextNode {
902 self.inner.borrow().layout.draw_node()
903 }
904
905 fn supports_text(&self, value: &str) -> bool {
906 let charset = self.inner.borrow().charset.clone();
907 if charset.is_empty() {
908 return true;
909 }
910 value.chars().all(|ch| charset.contains(ch))
911 }
912
913 fn include_in_charset(state: &mut DynamicTextLayoutState, value: &str) -> bool {
914 let mut changed = false;
915 for ch in value.chars() {
916 if !state.charset.contains(ch) {
917 state.charset.push(ch);
918 changed = true;
919 }
920 }
921 changed
922 }
923
924 fn refresh_numeric_text(&self) {
925 let (numeric_mode, has_value, numeric_value) = {
926 let state = self.inner.borrow();
927 (
928 state.numeric_mode,
929 state.has_numeric_value,
930 state.numeric_value,
931 )
932 };
933 if !numeric_mode || !has_value {
934 return;
935 }
936 let _ = self.set_text(self.compose_numeric_text(numeric_value));
937 }
938
939 fn compose_numeric_text(&self, value: f64) -> String {
940 let mut state = self.inner.borrow_mut();
941 let prefix = state.numeric_prefix.clone();
942 let suffix = state.numeric_suffix.clone();
943 let charset_changed = Self::include_in_charset(&mut state, &prefix)
944 | Self::include_in_charset(&mut state, &suffix);
945 let charset = state.charset.clone();
946 let layout = state.layout.clone();
947 drop(state);
948 if charset_changed {
949 layout.set_dynamic_charset_internal(charset);
950 }
951 format!("{prefix}{}{suffix}", self.format_numeric_value(value))
952 }
953
954 fn format_numeric_value(&self, value: f64) -> String {
955 let precision = self.inner.borrow().numeric_precision;
956 if value.is_nan() || !value.is_finite() || precision < 0 {
957 return value.to_string();
958 }
959 format!("{value:.precision$}", precision = precision as usize)
960 }
961}
962
963pub(crate) fn notify_font_loaded(_font_id: u32) {
964 PENDING_FONT_LAYOUTS.with(|layouts| {
965 let mut layouts = layouts.borrow_mut();
966 layouts.retain(|weak| {
967 let Some(inner) = weak.upgrade() else {
968 return false;
969 };
970 let layout = TextLayout { inner };
971 if layout.fonts_ready() {
972 layout.prepare_now();
973 false
974 } else {
975 true
976 }
977 });
978 });
979}
980
981#[cfg(test)]
982mod tests {
983 use super::{
984 notify_font_loaded, span, DynamicTextLayout, DynamicTextOverflow, RichText, TextLayout,
985 };
986 use crate::assets;
987 use crate::ffi::{self, Call, TextAlign, TextOverflow, TextVerticalAlign, Unit};
988 use crate::frame_scheduler;
989 use crate::node::Node;
990 use std::cell::RefCell;
991 use std::rc::Rc;
992
993 #[test]
994 fn rich_text_emits_style_runs() {
995 ffi::test::reset();
996 let node = RichText::new(vec![span("Hello")
997 .font_stack(crate::typography::FontStack::from_id(7), 18.0)
998 .text_color(0xFF00FFFF)]);
999 node.build();
1000 let calls = ffi::test::take_calls();
1001 assert!(calls
1002 .iter()
1003 .any(|call| matches!(call, Call::SetTextStyleRuns { run_count: 1, .. })));
1004 }
1005
1006 #[test]
1007 fn rich_text_retained_mutations_update_text_and_style_runs() {
1008 ffi::test::reset();
1009 let node = RichText::from_text("Before");
1010 node.build();
1011 ffi::test::take_calls();
1012
1013 node.fragments_value(vec![
1014 span("After").font_stack(crate::typography::FontStack::from_id(8), 19.0),
1015 span(" ✅").text_color(0x00FF00FF),
1016 ]);
1017
1018 let calls = ffi::test::take_calls();
1019 assert!(calls
1020 .iter()
1021 .any(|call| matches!(call, Call::SetText { text, .. } if text == "After ✅")));
1022 assert!(calls
1023 .iter()
1024 .any(|call| matches!(call, Call::SetTextStyleRuns { run_count: 2, .. })));
1025 }
1026
1027 #[test]
1028 fn rich_text_inherits_fui_as_max_lines_surface() {
1029 ffi::test::reset();
1030 let node = RichText::from_text("One line");
1031 node.max_lines(1).build();
1032 assert!(ffi::test::take_calls().iter().any(|call| matches!(
1033 call,
1034 Call::SetTextLimits {
1035 max_chars,
1036 max_lines,
1037 ..
1038 } if *max_chars == i32::MAX && *max_lines == 1
1039 )));
1040 }
1041
1042 #[test]
1043 fn rich_text_shared_surface_preserves_spans_and_rebuilds_base_typography() {
1044 ffi::test::reset();
1045 let node = RichText::new(vec![span("Bold").bold(), span(" plain")]);
1046 crate::node::TextTypographySurface::font_size(&node, 23.0);
1047 crate::node::TextLayoutSurface::wrapping(&node, true);
1048 crate::node::TextSelectionSurface::selection_range(&node, 1, 4);
1049 node.build();
1050
1051 let calls = ffi::test::take_calls();
1052 assert!(calls.iter().any(|call| matches!(
1053 call,
1054 Call::SetText { text, .. } if text == "Bold plain"
1055 )));
1056 assert!(calls
1057 .iter()
1058 .any(|call| matches!(call, Call::SetTextStyleRuns { run_count: 2, .. })));
1059 assert!(calls.iter().any(|call| matches!(
1060 call,
1061 Call::SetFont { size, .. } if (*size - 23.0).abs() < f32::EPSILON
1062 )));
1063 assert!(calls.iter().any(|call| matches!(
1064 call,
1065 Call::SetTextSelectionRange {
1066 start: 1,
1067 end: 4,
1068 ..
1069 }
1070 )));
1071 }
1072
1073 #[test]
1074 fn text_layout_waits_for_loaded_and_fonts_before_reporting_ready() {
1075 ffi::test::reset();
1076 frame_scheduler::reset_commit_state();
1077 ffi::test::set_text_metrics(88.0, 22.0, 16.0, 2, 64.0);
1078
1079 let ready_count = Rc::new(RefCell::new(0));
1080 let layout = TextLayout::text("Layout");
1081 layout
1082 .font_stack(crate::typography::FontStack::from_id(99), 16.0)
1083 .on_ready({
1084 let ready_count = ready_count.clone();
1085 move |_| *ready_count.borrow_mut() += 1
1086 });
1087
1088 frame_scheduler::fire_loaded_callbacks();
1089 assert_eq!(*ready_count.borrow(), 0);
1090
1091 assets::on_font_loaded(99);
1092 notify_font_loaded(99);
1093 assert_eq!(*ready_count.borrow(), 1);
1094
1095 let calls = ffi::test::take_calls();
1096 assert!(calls
1097 .iter()
1098 .any(|call| matches!(call, Call::PrepareNode { .. })));
1099 assert!(calls
1100 .iter()
1101 .any(|call| matches!(call, Call::GetTextMetrics { .. })));
1102 }
1103
1104 #[test]
1105 fn text_layout_emits_display_configuration() {
1106 ffi::test::reset();
1107 frame_scheduler::reset_commit_state();
1108 assets::on_font_loaded(1);
1109 TextLayout::text("Layout")
1110 .line_height(26.0)
1111 .text_align(TextAlign::Right)
1112 .text_vertical_align(TextVerticalAlign::Center)
1113 .text_limits(12, 3)
1114 .wrapping(true)
1115 .text_overflow(TextOverflow::Ellipsis)
1116 .text_overflow_fade(true, false)
1117 .on_ready(|_| {});
1118 frame_scheduler::fire_loaded_callbacks();
1119 let calls = ffi::test::take_calls();
1120 assert!(calls.iter().any(|call| matches!(call, Call::SetLineHeight { line_height, .. } if (*line_height - 26.0).abs() < f32::EPSILON)));
1121 assert!(calls.iter().any(|call| matches!(call, Call::SetTextAlign { align_enum, .. } if *align_enum == TextAlign::Right as u32)));
1122 assert!(calls.iter().any(|call| matches!(call, Call::SetTextVerticalAlign { align_enum, .. } if *align_enum == TextVerticalAlign::Center as u32)));
1123 assert!(calls.iter().any(|call| matches!(call, Call::SetTextLimits { max_chars, max_lines, .. } if *max_chars == 12 && *max_lines == 3)));
1124 assert!(calls
1125 .iter()
1126 .any(|call| matches!(call, Call::SetTextWrapping { wrap: true, .. })));
1127 assert!(calls.iter().any(|call| matches!(call, Call::SetTextOverflow { overflow_enum, .. } if *overflow_enum == TextOverflow::Ellipsis as u32)));
1128 assert!(calls.iter().any(|call| matches!(
1129 call,
1130 Call::SetTextOverflowFade {
1131 horizontal: true,
1132 vertical: false,
1133 ..
1134 }
1135 )));
1136 }
1137
1138 #[test]
1139 fn dynamic_text_layout_rejects_unsupported_text_when_configured() {
1140 ffi::test::reset();
1141 let layout = DynamicTextLayout::fixed_charset("0123456789");
1142 layout.overflow(DynamicTextOverflow::Reject);
1143 assert!(layout.set_text("123"));
1144 assert!(!layout.set_text("12a"));
1145 }
1146
1147 #[test]
1148 fn dynamic_text_layout_reprepares_after_ready_text_update() {
1149 ffi::test::reset();
1150 frame_scheduler::reset_commit_state();
1151 assets::test_reset();
1152
1153 let layout = DynamicTextLayout::fixed_charset("0123456789");
1154 layout
1155 .font_stack(crate::typography::FontStack::from_id(1), 16.0)
1156 .width(120.0, Unit::Pixel)
1157 .height(24.0, Unit::Pixel)
1158 .on_ready(|_| {});
1159 frame_scheduler::fire_loaded_callbacks();
1160 assert!(layout.is_ready());
1161 ffi::test::take_calls();
1162
1163 assert!(layout.set_text("42"));
1164 assert!(layout.is_ready());
1165 let calls = ffi::test::take_calls();
1166 assert!(calls
1167 .iter()
1168 .any(|call| matches!(call, Call::PrepareNode { .. })));
1169 assert!(calls
1170 .iter()
1171 .any(|call| matches!(call, Call::SetDynamicTextCharset { charset, .. } if charset == "0123456789")));
1172 }
1173
1174 #[test]
1175 fn numeric_dynamic_text_layout_stays_ready_across_value_updates() {
1176 ffi::test::reset();
1177 frame_scheduler::reset_commit_state();
1178 assets::test_reset();
1179
1180 let layout = DynamicTextLayout::numeric();
1181 layout
1182 .precision(0)
1183 .font_stack(crate::typography::FontStack::from_id(1), 16.0)
1184 .width(120.0, Unit::Pixel)
1185 .height(24.0, Unit::Pixel)
1186 .on_ready(|_| {});
1187 frame_scheduler::fire_loaded_callbacks();
1188 assert!(layout.is_ready());
1189 ffi::test::take_calls();
1190
1191 assert!(layout.set_value(42.0));
1192 assert!(layout.is_ready());
1193 assert_eq!(layout.current_text(), "42");
1194 let calls = ffi::test::take_calls();
1195 assert!(calls
1196 .iter()
1197 .any(|call| matches!(call, Call::PrepareNode { .. })));
1198 }
1199}