Skip to main content

fui/
text.rs

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}