Skip to main content

fui/node/
text_node.rs

1use super::core::*;
2use super::*;
3use crate::event::{SelectionChangedEventArgs, TextChangedEventArgs};
4use crate::text_indices::{byte_to_scalar, scalar_count, scalar_to_byte};
5use crate::{FontFamily, FontStack, FontStyle, FontWeight};
6
7type TextChangedCallback = Rc<dyn Fn(TextChangedEventArgs)>;
8type TextReplacedCallback = Rc<dyn Fn(u32, u32, String)>;
9type SelectionChangedCallback = Rc<dyn Fn(SelectionChangedEventArgs)>;
10
11#[derive(Clone)]
12pub struct TextNode {
13    core: Rc<RefCell<NodeCore>>,
14    props: Rc<RefCell<TextProps>>,
15    text_changed_callback: Rc<RefCell<Option<TextChangedCallback>>>,
16    text_replaced_callback: Rc<RefCell<Option<TextReplacedCallback>>>,
17    selection_changed_callback: Rc<RefCell<Option<SelectionChangedCallback>>>,
18}
19
20impl TextNode {
21    pub fn new(content: impl Into<String>) -> Self {
22        Self::new_with_defaults(content, true)
23    }
24
25    pub(crate) fn new_core(content: impl Into<String>) -> Self {
26        Self::new_with_defaults(content, false)
27    }
28
29    fn new_with_defaults(content: impl Into<String>, selectable_by_default: bool) -> Self {
30        let content = content.into();
31        let core = Rc::new(RefCell::new(NodeCore::new(NodeKind::Text)));
32        let theme = theme::current_theme();
33        let props = Rc::new(RefCell::new(TextProps {
34            content: content.clone(),
35            font_size: theme.fonts.size_body,
36            has_font: true,
37            selectable: selectable_by_default.then_some((true, theme.colors.selection)),
38            uses_theme_selection_color: selectable_by_default,
39            ..TextProps::default()
40        }));
41        {
42            let mut core_mut = core.borrow_mut();
43            if selectable_by_default {
44                core_mut.behavior.cursor = Some(CursorStyle::Text);
45                core_mut.behavior.selectable_text = true;
46            }
47            core_mut.behavior.text_content = Some(content.clone());
48            let weak_props = Rc::downgrade(&props);
49            core_mut.behavior.text_selection_range_bytes = Some(Rc::new(move || {
50                weak_props
51                    .upgrade()
52                    .and_then(|props| props.borrow().selection_range_bytes)
53                    .unwrap_or((0, 0))
54            }));
55        }
56        let node = Self {
57            core,
58            props,
59            text_changed_callback: Rc::new(RefCell::new(None)),
60            text_replaced_callback: Rc::new(RefCell::new(None)),
61            selection_changed_callback: Rc::new(RefCell::new(None)),
62        };
63        node.install_runtime_state_handlers();
64        node.bind_theme_defaults();
65        node
66    }
67
68    pub fn key(&self, _key: u64) -> &Self {
69        self
70    }
71
72    pub fn width(&self, width: f32, unit: Unit) -> &Self {
73        self.props.borrow_mut().width = Some((width, unit));
74        {
75            let mut core = self.core.borrow_mut();
76            core.behavior.fill_width = false;
77            core.behavior.fill_width_percent = None;
78        }
79        if self.has_built_handle() {
80            ui::set_width(self.handle().raw(), width, unit as u32);
81            self.notify_retained_layout_mutation();
82        }
83        self
84    }
85
86    pub fn height(&self, height: f32, unit: Unit) -> &Self {
87        self.props.borrow_mut().height = Some((height, unit));
88        {
89            let mut core = self.core.borrow_mut();
90            core.behavior.fill_height = false;
91            core.behavior.fill_height_percent = None;
92        }
93        if self.has_built_handle() {
94            ui::set_height(self.handle().raw(), height, unit as u32);
95            self.notify_retained_layout_mutation();
96        }
97        self
98    }
99
100    pub fn fill_width(&self) -> &Self {
101        self.props.borrow_mut().width = None;
102        {
103            let mut core = self.core.borrow_mut();
104            core.behavior.fill_width = true;
105            core.behavior.fill_width_percent = None;
106        }
107        if self.has_built_handle() {
108            ui::set_fill_width(self.handle().raw(), true);
109            self.notify_retained_layout_mutation();
110        }
111        self
112    }
113
114    pub fn fill_height(&self) -> &Self {
115        self.props.borrow_mut().height = None;
116        {
117            let mut core = self.core.borrow_mut();
118            core.behavior.fill_height = true;
119            core.behavior.fill_height_percent = None;
120        }
121        if self.has_built_handle() {
122            ui::set_fill_height(self.handle().raw(), true);
123            self.notify_retained_layout_mutation();
124        }
125        self
126    }
127
128    pub fn fill_size(&self) -> &Self {
129        self.fill_width();
130        self.fill_height();
131        self
132    }
133
134    pub fn fill_width_percent(&self, percent: f32) -> &Self {
135        self.props.borrow_mut().width = None;
136        {
137            let mut core = self.core.borrow_mut();
138            core.behavior.fill_width = false;
139            core.behavior.fill_width_percent = Some(percent);
140        }
141        if self.has_built_handle() {
142            ui::set_fill_width_percent(self.handle().raw(), percent);
143            self.notify_retained_layout_mutation();
144        }
145        self
146    }
147
148    pub fn fill_height_percent(&self, percent: f32) -> &Self {
149        self.props.borrow_mut().height = None;
150        {
151            let mut core = self.core.borrow_mut();
152            core.behavior.fill_height = false;
153            core.behavior.fill_height_percent = Some(percent);
154        }
155        if self.has_built_handle() {
156            ui::set_fill_height_percent(self.handle().raw(), percent);
157            self.notify_retained_layout_mutation();
158        }
159        self
160    }
161
162    pub fn min_width(&self, value: f32, unit: Unit) -> &Self {
163        self.props.borrow_mut().min_width = Some((value, unit));
164        if self.has_built_handle() {
165            ui::set_min_width(self.handle().raw(), value, unit as u32);
166            self.notify_retained_layout_mutation();
167        }
168        self
169    }
170
171    pub fn max_width(&self, value: f32, unit: Unit) -> &Self {
172        self.props.borrow_mut().max_width = Some((value, unit));
173        if self.has_built_handle() {
174            ui::set_max_width(self.handle().raw(), value, unit as u32);
175            self.notify_retained_layout_mutation();
176        }
177        self
178    }
179
180    pub fn min_height(&self, value: f32, unit: Unit) -> &Self {
181        self.props.borrow_mut().min_height = Some((value, unit));
182        if self.has_built_handle() {
183            ui::set_min_height(self.handle().raw(), value, unit as u32);
184            self.notify_retained_layout_mutation();
185        }
186        self
187    }
188
189    pub fn max_height(&self, value: f32, unit: Unit) -> &Self {
190        self.props.borrow_mut().max_height = Some((value, unit));
191        if self.has_built_handle() {
192            ui::set_max_height(self.handle().raw(), value, unit as u32);
193            self.notify_retained_layout_mutation();
194        }
195        self
196    }
197
198    pub fn text(&self, content: impl Into<String>) -> &Self {
199        let content = content.into();
200        Self::sync_content_state(&self.core, &self.props, content.clone());
201        if self.has_built_handle() {
202            ui::set_text(self.handle().raw(), &content);
203            self.notify_retained_layout_mutation();
204        }
205        self
206    }
207
208    pub fn text_color(&self, color: u32) -> &Self {
209        self.props.borrow_mut().text_color = Some(color);
210        if self.has_built_handle() {
211            ui::set_text_color(self.handle().raw(), color);
212            self.notify_retained_mutation();
213        }
214        self
215    }
216
217    pub fn style_runs(&self, words: Vec<u32>) -> &Self {
218        {
219            let mut props = self.props.borrow_mut();
220            props.style_runs = words;
221            props.has_style_runs = true;
222        }
223        if self.has_built_handle() {
224            let props = self.props.borrow();
225            ui::set_text_style_runs(self.handle().raw(), &props.style_runs);
226            self.notify_retained_layout_mutation();
227        }
228        self
229    }
230
231    pub(crate) fn font_id(&self, font_id: u32, size: f32) -> &Self {
232        let mut props = self.props.borrow_mut();
233        props.uses_direct_font_id = true;
234        props.font_family = None;
235        props.font_id = font_id;
236        props.font_size = size;
237        props.has_font = true;
238        drop(props);
239        if self.has_built_handle() {
240            self.apply_resolved_font();
241        }
242        self
243    }
244
245    pub fn font_stack(&self, stack: FontStack, size: f32) -> &Self {
246        self.font_id(stack.id(), size)
247    }
248
249    pub fn font_family(&self, family: FontFamily) -> &Self {
250        let mut props = self.props.borrow_mut();
251        props.font_family = Some(family);
252        props.uses_direct_font_id = false;
253        drop(props);
254        if self.has_built_handle() && self.props.borrow().has_font {
255            self.apply_resolved_font();
256        }
257        self
258    }
259
260    pub fn font_weight(&self, weight: FontWeight) -> &Self {
261        self.props.borrow_mut().font_weight = weight;
262        if self.has_built_handle() && self.props.borrow().has_font {
263            self.apply_resolved_font();
264        }
265        self
266    }
267
268    pub fn font_style(&self, style: FontStyle) -> &Self {
269        self.props.borrow_mut().font_style = style;
270        if self.has_built_handle() && self.props.borrow().has_font {
271            self.apply_resolved_font();
272        }
273        self
274    }
275
276    pub fn font_size(&self, size: f32) -> &Self {
277        let mut props = self.props.borrow_mut();
278        props.font_size = size;
279        props.has_font = true;
280        drop(props);
281        if self.has_built_handle() {
282            self.apply_resolved_font();
283        }
284        self
285    }
286
287    pub fn line_height(&self, line_height: f32) -> &Self {
288        self.props.borrow_mut().line_height = Some(line_height);
289        if self.has_built_handle() {
290            ui::set_line_height(self.handle().raw(), line_height);
291            self.notify_retained_layout_mutation();
292        }
293        self
294    }
295
296    pub fn text_align(&self, align: TextAlign) -> &Self {
297        self.props.borrow_mut().text_align = Some(align);
298        if self.has_built_handle() {
299            ui::set_text_align(self.handle().raw(), align as u32);
300            self.notify_retained_layout_mutation();
301        }
302        self
303    }
304
305    pub fn text_vertical_align(&self, align: TextVerticalAlign) -> &Self {
306        self.props.borrow_mut().text_vertical_align = Some(align);
307        if self.has_built_handle() {
308            ui::set_text_vertical_align(self.handle().raw(), align as u32);
309            self.notify_retained_layout_mutation();
310        }
311        self
312    }
313
314    pub fn text_limits(&self, max_chars: i32, max_lines: i32) -> &Self {
315        self.props.borrow_mut().text_limits = Some((max_chars, max_lines));
316        if self.has_built_handle() {
317            ui::set_text_limits(self.handle().raw(), max_chars, max_lines);
318            self.notify_retained_layout_mutation();
319        }
320        self
321    }
322
323    pub fn max_lines(&self, max_lines: i32) -> &Self {
324        self.text_limits(i32::MAX, max_lines)
325    }
326
327    pub fn wrapping(&self, wrap: bool) -> &Self {
328        self.props.borrow_mut().wrapping = Some(wrap);
329        if self.has_built_handle() {
330            ui::set_text_wrapping(self.handle().raw(), wrap);
331            self.notify_retained_layout_mutation();
332        }
333        self
334    }
335
336    pub fn text_overflow(&self, overflow: TextOverflow) -> &Self {
337        self.props.borrow_mut().overflow = Some(overflow);
338        if self.has_built_handle() {
339            ui::set_text_overflow(self.handle().raw(), overflow as u32);
340            self.notify_retained_layout_mutation();
341        }
342        self
343    }
344
345    pub fn text_overflow_fade(&self, horizontal: bool, vertical: bool) -> &Self {
346        self.props.borrow_mut().overflow_fade = Some((horizontal, vertical));
347        if self.has_built_handle() {
348            ui::set_text_overflow_fade(self.handle().raw(), horizontal, vertical);
349            self.notify_retained_layout_mutation();
350        }
351        self
352    }
353
354    pub fn selectable(&self, selectable: bool) -> &Self {
355        let mut props = self.props.borrow_mut();
356        let resolved_selection_color = props
357            .selectable
358            .map(|(_, color)| color)
359            .unwrap_or_else(|| theme::current_theme().colors.selection);
360        props.selectable = Some((selectable, resolved_selection_color));
361        drop(props);
362        {
363            let mut core = self.core.borrow_mut();
364            core.behavior.selectable_text = selectable;
365            if selectable {
366                core.behavior.cursor = Some(CursorStyle::Text);
367            } else if core.behavior.cursor == Some(CursorStyle::Text) {
368                core.behavior.cursor = Some(CursorStyle::Default);
369            }
370        }
371        crate::event::handle_cursor_style_changed(self.handle());
372        if self.has_built_handle() {
373            ui::set_selectable(self.handle().raw(), selectable, resolved_selection_color);
374            self.notify_retained_mutation();
375        }
376        self
377    }
378
379    pub fn selection_color(&self, color: u32) -> &Self {
380        let mut props = self.props.borrow_mut();
381        let selectable = props.selectable.map(|(value, _)| value).unwrap_or(false);
382        props.selectable = Some((selectable, color));
383        props.uses_theme_selection_color = false;
384        drop(props);
385        if self.has_built_handle() {
386            ui::set_selectable(self.handle().raw(), selectable, color);
387            self.notify_retained_mutation();
388        }
389        self
390    }
391
392    pub fn editable(&self, editable: bool) -> &Self {
393        if editable && self.props.borrow().selectable.is_none() {
394            let selection_color = self
395                .props
396                .borrow()
397                .selectable
398                .map(|(_, color)| color)
399                .unwrap_or(theme::current_theme().colors.selection);
400            self.props.borrow_mut().selectable = Some((true, selection_color));
401            self.core.borrow_mut().behavior.selectable_text = true;
402        }
403        self.props.borrow_mut().editable = Some(editable);
404        self.core.borrow_mut().behavior.editable_text = editable;
405        if self.has_built_handle() {
406            ui::set_editable(self.handle().raw(), editable);
407            self.notify_retained_mutation();
408        }
409        self
410    }
411
412    pub(crate) fn editor_command_keys(&self, enabled: bool) -> &Self {
413        self.props.borrow_mut().editor_command_keys = Some(enabled);
414        if self.has_built_handle() {
415            ui::set_editor_command_keys(self.handle().raw(), enabled);
416            self.notify_retained_mutation();
417        }
418        self
419    }
420
421    pub(crate) fn editor_accepts_tab(&self, enabled: bool) -> &Self {
422        self.props.borrow_mut().editor_accepts_tab = Some(enabled);
423        if self.has_built_handle() {
424            ui::set_editor_accepts_tab(self.handle().raw(), enabled);
425            self.notify_retained_mutation();
426        }
427        self
428    }
429
430    pub fn obscured(&self, obscured: bool) -> &Self {
431        self.props.borrow_mut().obscured = Some(obscured);
432        if self.has_built_handle() {
433            ui::set_text_obscured(self.handle().raw(), obscured);
434            self.notify_retained_mutation();
435        }
436        self
437    }
438
439    pub fn caret_color(&self, color: u32) -> &Self {
440        self.props.borrow_mut().caret_color = Some(color);
441        if self.has_built_handle() {
442            ui::set_caret_color(self.handle().raw(), color);
443            self.notify_retained_mutation();
444        }
445        self
446    }
447
448    pub fn selection_range(&self, start: u32, end: u32) -> &Self {
449        let mut props = self.props.borrow_mut();
450        let start = start.min(scalar_count(&props.content));
451        let end = end.min(scalar_count(&props.content));
452        let start_byte = scalar_to_byte(&props.content, start);
453        let end_byte = scalar_to_byte(&props.content, end);
454        props.selection_start = start;
455        props.selection_end = end;
456        props.selection_range_bytes = Some((start_byte, end_byte));
457        drop(props);
458        if self.has_built_handle() {
459            ui::set_text_selection_range(self.handle().raw(), start_byte, end_byte);
460            self.notify_retained_mutation();
461        }
462        self
463    }
464
465    pub fn focus_now(&self) -> &Self {
466        if self.has_built_handle() {
467            ui::request_focus(self.handle().raw());
468        }
469        self
470    }
471
472    pub(crate) fn default_semantic_label(&self, label: impl Into<String>) -> &Self {
473        let label = label.into();
474        let mut core = self.core.borrow_mut();
475        core.behavior.default_semantic_label = Some(label.clone());
476        if core.behavior.semantic_label.is_none() && core.handle != NodeHandle::INVALID {
477            ui::set_semantic_label(core.handle.raw(), &label);
478            drop(core);
479            self.notify_retained_mutation();
480        }
481        self
482    }
483
484    pub fn interactive(&self, interactive: bool) -> &Self {
485        self.core.borrow_mut().behavior.interactive = interactive;
486        self
487    }
488
489    pub fn cursor(&self, style: CursorStyle) -> &Self {
490        self.core.borrow_mut().behavior.cursor = Some(style);
491        crate::event::handle_cursor_style_changed(self.handle());
492        self
493    }
494
495    pub fn focusable(&self, enabled: bool, tab_index: i32) -> &Self {
496        if enabled {
497            self.retained_node_ref().require_interactive();
498        }
499        let mut core = self.core.borrow_mut();
500        core.behavior.focusable = Some((enabled, tab_index));
501        let interactive = core.behavior.enabled && core.behavior.inherited_enabled;
502        let handle = core.handle;
503        drop(core);
504        if handle != NodeHandle::INVALID {
505            ui::set_focusable(handle.raw(), interactive && enabled, tab_index);
506            self.notify_retained_mutation();
507        }
508        self
509    }
510
511    pub(crate) fn reflect_semantic_disabled_from_enabled(&self) -> &Self {
512        let mut core = self.core.borrow_mut();
513        core.behavior.track_semantic_disabled_from_enabled = true;
514        let effective_enabled = core.behavior.enabled && core.behavior.inherited_enabled;
515        let handle = core.handle;
516        drop(core);
517        if handle != NodeHandle::INVALID {
518            ui::set_semantic_disabled(handle.raw(), true, !effective_enabled);
519            self.notify_retained_mutation();
520        }
521        self
522    }
523
524    pub fn on_pointer_click(&self, handler: impl Fn(&mut PointerEventArgs) + 'static) -> &Self {
525        self.core.borrow_mut().handlers.pointer_click = Some(Rc::new(handler));
526        self.retained_node_ref().require_interactive();
527        self
528    }
529
530    pub fn on_pointer_down(&self, handler: impl Fn(&mut PointerEventArgs) + 'static) -> &Self {
531        self.core.borrow_mut().handlers.pointer_down = Some(Rc::new(handler));
532        self.retained_node_ref().require_interactive();
533        self
534    }
535
536    pub fn on_pointer_up(&self, handler: impl Fn(&mut PointerEventArgs) + 'static) -> &Self {
537        self.core.borrow_mut().handlers.pointer_up = Some(Rc::new(handler));
538        self.retained_node_ref().require_interactive();
539        self
540    }
541
542    pub fn on_focus_changed(&self, handler: impl Fn(FocusChangedEventArgs) + 'static) -> &Self {
543        self.core.borrow_mut().handlers.focus_changed = Some(Rc::new(handler));
544        self
545    }
546
547    pub fn on_text_changed(&self, handler: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
548        *self.text_changed_callback.borrow_mut() = Some(Rc::new(handler));
549        self
550    }
551
552    pub(crate) fn on_text_replaced(&self, handler: impl Fn(u32, u32, String) + 'static) -> &Self {
553        *self.text_replaced_callback.borrow_mut() = Some(Rc::new(handler));
554        self
555    }
556
557    pub fn on_selection_changed(
558        &self,
559        handler: impl Fn(SelectionChangedEventArgs) + 'static,
560    ) -> &Self {
561        *self.selection_changed_callback.borrow_mut() = Some(Rc::new(handler));
562        self
563    }
564
565    pub fn content(&self) -> String {
566        self.props.borrow().content.clone()
567    }
568
569    pub fn uses_default_selection_behavior(&self) -> bool {
570        self.props.borrow().selectable.is_none()
571    }
572
573    pub fn is_editable_text(&self) -> bool {
574        self.props.borrow().editable.unwrap_or(false)
575    }
576
577    pub fn is_selectable_text(&self) -> bool {
578        self.props
579            .borrow()
580            .selectable
581            .map(|(selectable, _)| selectable)
582            .unwrap_or(false)
583    }
584
585    pub fn selection_start(&self) -> u32 {
586        self.props.borrow().selection_start
587    }
588
589    pub fn selection_end(&self) -> u32 {
590        self.props.borrow().selection_end
591    }
592
593    pub fn on_pan_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
594        self.core.borrow_mut().handlers.pan_gesture = Some(Rc::new(handler));
595        self
596    }
597
598    pub fn on_pinch_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
599        self.core.borrow_mut().handlers.pinch_gesture = Some(Rc::new(handler));
600        self
601    }
602
603    pub fn long_press_options(&self, minimum_duration_ms: i32, movement_tolerance: f32) -> &Self {
604        let mut core = self.core.borrow_mut();
605        core.handlers.long_press_minimum_duration_ms = minimum_duration_ms.max(0);
606        core.handlers.long_press_movement_tolerance = movement_tolerance.max(0.0);
607        self
608    }
609
610    pub fn on_long_press(&self, handler: impl Fn(&mut LongPressEventArgs) + 'static) -> &Self {
611        self.core.borrow_mut().handlers.long_press = Some(Rc::new(handler));
612        self
613    }
614
615    fn install_runtime_state_handlers(&self) {
616        let weak_core = Rc::downgrade(&self.core);
617        let weak_props = Rc::downgrade(&self.props);
618        let weak_callback = Rc::downgrade(&self.text_changed_callback);
619        self.core.borrow_mut().handlers.text_changed = Some(Rc::new(move |event| {
620            let (Some(core), Some(props)) = (weak_core.upgrade(), weak_props.upgrade()) else {
621                return;
622            };
623            Self::sync_content_state(&core, &props, event.text.clone());
624            if let Some(callbacks) = weak_callback.upgrade() {
625                let callback = callbacks.borrow().clone();
626                if let Some(callback) = callback {
627                    callback(event);
628                }
629            }
630        }));
631
632        let weak_core = Rc::downgrade(&self.core);
633        let weak_props = Rc::downgrade(&self.props);
634        let weak_changed_callback = Rc::downgrade(&self.text_changed_callback);
635        let weak_replaced_callback = Rc::downgrade(&self.text_replaced_callback);
636        self.core.borrow_mut().handlers.text_replaced =
637            Some(Rc::new(move |start, end, replacement| {
638                let (Some(core), Some(props)) = (weak_core.upgrade(), weak_props.upgrade()) else {
639                    return;
640                };
641                let content = {
642                    let current = props.borrow().content.clone();
643                    let start_byte = scalar_to_byte(&current, byte_to_scalar(&current, start));
644                    let end_byte =
645                        scalar_to_byte(&current, byte_to_scalar(&current, start.max(end)));
646                    let mut updated = current;
647                    updated.replace_range(start_byte as usize..end_byte as usize, &replacement);
648                    updated
649                };
650                Self::sync_content_state(&core, &props, content.clone());
651                if let Some(callbacks) = weak_changed_callback.upgrade() {
652                    let callback = callbacks.borrow().clone();
653                    if let Some(callback) = callback {
654                        callback(TextChangedEventArgs {
655                            text: content.clone(),
656                        });
657                    }
658                }
659                if let Some(callbacks) = weak_replaced_callback.upgrade() {
660                    let callback = callbacks.borrow().clone();
661                    if let Some(callback) = callback {
662                        callback(start, end, replacement);
663                    }
664                }
665            }));
666
667        let weak_props = Rc::downgrade(&self.props);
668        let weak_callback = Rc::downgrade(&self.selection_changed_callback);
669        self.core.borrow_mut().handlers.selection_changed = Some(Rc::new(move |event| {
670            let Some(props) = weak_props.upgrade() else {
671                return;
672            };
673            let (start, end) = {
674                let mut props = props.borrow_mut();
675                let start = byte_to_scalar(&props.content, event.start);
676                let end = byte_to_scalar(&props.content, event.end);
677                let start_byte = scalar_to_byte(&props.content, start);
678                let end_byte = scalar_to_byte(&props.content, end);
679                props.selection_start = start;
680                props.selection_end = end;
681                props.selection_range_bytes = Some((start_byte, end_byte));
682                (start, end)
683            };
684            if let Some(callbacks) = weak_callback.upgrade() {
685                let callback = callbacks.borrow().clone();
686                if let Some(callback) = callback {
687                    callback(SelectionChangedEventArgs { start, end });
688                }
689            }
690        }));
691    }
692
693    fn sync_content_state(
694        core: &Rc<RefCell<NodeCore>>,
695        props: &Rc<RefCell<TextProps>>,
696        content: String,
697    ) {
698        {
699            let mut props = props.borrow_mut();
700            let length = scalar_count(&content);
701            props.selection_start = props.selection_start.min(length);
702            props.selection_end = props.selection_end.min(length);
703            if props.selection_range_bytes.is_some() {
704                props.selection_range_bytes = Some((
705                    scalar_to_byte(&content, props.selection_start),
706                    scalar_to_byte(&content, props.selection_end),
707                ));
708            }
709            props.content = content.clone();
710        }
711        core.borrow_mut().behavior.text_content = Some(content);
712    }
713
714    pub(crate) fn required_font_ids(&self) -> Vec<u32> {
715        let props = self.props.borrow();
716        let mut font_ids = Vec::new();
717        if props.has_font && props.font_id != 0 {
718            font_ids.push(props.font_id);
719        }
720        for chunk in props.style_runs.chunks_exact(7) {
721            let font_id = chunk[2];
722            if !font_ids.contains(&font_id) {
723                font_ids.push(font_id);
724            }
725        }
726        font_ids
727    }
728
729    fn bind_theme_defaults(&self) {
730        let weak_core = Rc::downgrade(&self.core);
731        let weak_props = Rc::downgrade(&self.props);
732        let guard = theme::subscribe(move |theme| {
733            let Some(core) = weak_core.upgrade() else {
734                return;
735            };
736            let Some(props) = weak_props.upgrade() else {
737                return;
738            };
739            TextNode::handle_theme_changed(&core, &props, &theme);
740        });
741        self.retained_node_ref().retain_attachment(Rc::new(guard));
742    }
743
744    fn handle_theme_changed(
745        core: &Rc<RefCell<NodeCore>>,
746        props: &Rc<RefCell<TextProps>>,
747        theme: &crate::theme::Theme,
748    ) {
749        let handle = core.borrow().handle;
750        if handle == NodeHandle::INVALID {
751            if props.borrow().has_font
752                && !props.borrow().uses_direct_font_id
753                && props.borrow().font_family.is_none()
754            {
755                let resolved_font_id = theme
756                    .fonts
757                    .body_family
758                    .resolve(props.borrow().font_weight, props.borrow().font_style);
759                props.borrow_mut().font_id = resolved_font_id;
760            }
761            return;
762        }
763
764        let mut should_request_render = false;
765        {
766            let mut text_props = props.borrow_mut();
767            if text_props.text_color.is_none() {
768                ui::set_text_color(handle.raw(), theme.colors.text_primary);
769                should_request_render = true;
770            }
771            if text_props.has_font
772                && !text_props.uses_direct_font_id
773                && text_props.font_family.is_none()
774            {
775                text_props.font_id = theme
776                    .fonts
777                    .body_family
778                    .resolve(text_props.font_weight, text_props.font_style);
779                ui::set_font(handle.raw(), text_props.font_id, text_props.font_size);
780                should_request_render = true;
781            }
782            if text_props.uses_theme_selection_color {
783                if let Some((selectable, _)) = text_props.selectable {
784                    text_props.selectable = Some((selectable, theme.colors.selection));
785                    ui::set_selectable(handle.raw(), selectable, theme.colors.selection);
786                    should_request_render = true;
787                }
788            }
789        }
790        if should_request_render {
791            crate::frame_scheduler::mark_needs_commit();
792        }
793    }
794
795    fn resolve_font_id(&self) -> u32 {
796        let mut props = self.props.borrow_mut();
797        if props.uses_direct_font_id {
798            return props.font_id;
799        }
800        let family = props
801            .font_family
802            .clone()
803            .unwrap_or_else(|| theme::current_theme().fonts.body_family.clone());
804        props.font_id = family.resolve(props.font_weight, props.font_style);
805        props.font_id
806    }
807
808    fn apply_resolved_font(&self) {
809        let font_id = self.resolve_font_id();
810        let font_size = self.props.borrow().font_size;
811        ui::set_font(self.handle().raw(), font_id, font_size);
812        self.notify_retained_layout_mutation();
813    }
814}
815
816impl Node for TextNode {
817    fn retained_node_ref(&self) -> NodeRef {
818        NodeRef::from_node(self.core.clone(), self.clone())
819    }
820
821    fn build_self(&self) {
822        if self.props.borrow().has_font {
823            self.resolve_font_id();
824        }
825        // Host setters may synchronously report text or selection state back into
826        // this retained node. Never hold a RefCell borrow across that boundary.
827        let props = self.props.borrow().clone();
828        apply_text_props(self.handle(), &props, self.core.borrow().behavior.clone());
829    }
830
831    fn required_font_ids_for_preparation(&self) -> Vec<u32> {
832        self.required_font_ids()
833    }
834}
835
836impl super::ThemeBindable for TextNode {
837    fn theme_binding_node(&self) -> NodeRef {
838        self.retained_node_ref()
839    }
840
841    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
842        let weak_core = Rc::downgrade(&self.core);
843        let weak_props = Rc::downgrade(&self.props);
844        let weak_text_changed_callback = Rc::downgrade(&self.text_changed_callback);
845        let weak_text_replaced_callback = Rc::downgrade(&self.text_replaced_callback);
846        let weak_selection_changed_callback = Rc::downgrade(&self.selection_changed_callback);
847        Box::new(move || {
848            Some(TextNode {
849                core: weak_core.upgrade()?,
850                props: weak_props.upgrade()?,
851                text_changed_callback: weak_text_changed_callback.upgrade()?,
852                text_replaced_callback: weak_text_replaced_callback.upgrade()?,
853                selection_changed_callback: weak_selection_changed_callback.upgrade()?,
854            })
855        })
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use crate::ffi::{self, Call};
863    use crate::Application;
864
865    #[test]
866    fn prebuild_font_family_weight_and_style_are_resolved_during_build() {
867        ffi::test::reset();
868        let theme = theme::current_theme();
869        let label = TextNode::new_core("Bold before build");
870        label
871            .font_family(theme.fonts.body_family)
872            .font_weight(FontWeight::Bold)
873            .font_style(FontStyle::Normal)
874            .font_size(17.0);
875
876        Application::mount(label.clone());
877        let handle = label.handle().raw();
878        let calls = ffi::test::take_calls();
879        assert!(calls.iter().any(|call| matches!(
880            call,
881            Call::SetFont {
882                handle: call_handle,
883                font_id: 2,
884                size,
885            } if *call_handle == handle && (*size - 17.0).abs() < f32::EPSILON
886        )));
887        Application::unmount();
888    }
889
890    #[test]
891    fn text_layout_surface_replays_before_build_and_mutates_after_build() {
892        ffi::test::reset();
893        let label = TextNode::new("Layout");
894        label
895            .fill_width_percent(75.0)
896            .fill_height_percent(50.0)
897            .min_width(12.0, Unit::Pixel)
898            .max_width(80.0, Unit::Percent)
899            .min_height(14.0, Unit::Pixel)
900            .max_height(90.0, Unit::Pixel)
901            .text_align(TextAlign::Center)
902            .text_vertical_align(TextVerticalAlign::Bottom)
903            .text_overflow(TextOverflow::Ellipsis)
904            .text_overflow_fade(true, false);
905
906        Application::mount(label.clone());
907        let calls = ffi::test::take_calls();
908        assert!(calls.iter().any(|call| matches!(call, Call::SetFillWidthPercent { percent, .. } if (*percent - 75.0).abs() < f32::EPSILON)));
909        assert!(calls.iter().any(|call| matches!(call, Call::SetFillHeightPercent { percent, .. } if (*percent - 50.0).abs() < f32::EPSILON)));
910        assert!(calls.iter().any(|call| matches!(call, Call::SetMinWidth { value, unit_enum, .. } if (*value - 12.0).abs() < f32::EPSILON && *unit_enum == Unit::Pixel as u32)));
911        assert!(calls.iter().any(|call| matches!(call, Call::SetMaxWidth { value, unit_enum, .. } if (*value - 80.0).abs() < f32::EPSILON && *unit_enum == Unit::Percent as u32)));
912        assert!(calls.iter().any(|call| matches!(call, Call::SetMinHeight { value, .. } if (*value - 14.0).abs() < f32::EPSILON)));
913        assert!(calls.iter().any(|call| matches!(call, Call::SetMaxHeight { value, .. } if (*value - 90.0).abs() < f32::EPSILON)));
914
915        label
916            .text_align(TextAlign::Right)
917            .text_vertical_align(TextVerticalAlign::Center)
918            .text_overflow(TextOverflow::Clip)
919            .text_overflow_fade(false, true);
920        let calls = ffi::test::take_calls();
921        assert!(calls.iter().any(|call| matches!(call, Call::SetTextAlign { align_enum, .. } if *align_enum == TextAlign::Right as u32)));
922        assert!(calls.iter().any(|call| matches!(call, Call::SetTextVerticalAlign { align_enum, .. } if *align_enum == TextVerticalAlign::Center as u32)));
923        assert!(calls.iter().any(|call| matches!(call, Call::SetTextOverflow { overflow_enum, .. } if *overflow_enum == TextOverflow::Clip as u32)));
924        assert!(calls.iter().any(|call| matches!(
925            call,
926            Call::SetTextOverflowFade {
927                horizontal: false,
928                vertical: true,
929                ..
930            }
931        )));
932        Application::unmount();
933    }
934
935    #[test]
936    fn public_selection_uses_scalar_indices_at_the_utf8_boundary() {
937        ffi::test::reset();
938        let label = TextNode::new("A你😀Z");
939        Application::mount(label.clone());
940        ffi::test::take_calls();
941
942        label.selection_range(2, 3);
943        assert_eq!(label.selection_start(), 2);
944        assert_eq!(label.selection_end(), 3);
945        assert!(ffi::test::take_calls().iter().any(|call| matches!(
946            call,
947            Call::SetTextSelectionRange {
948                start: 4,
949                end: 8,
950                ..
951            }
952        )));
953
954        let observed = Rc::new(RefCell::new(None));
955        label.on_selection_changed({
956            let observed = observed.clone();
957            move |event| *observed.borrow_mut() = Some((event.start, event.end))
958        });
959        crate::event::__fui_on_selection_changed(label.handle().raw(), 1, 8);
960        assert_eq!(label.selection_start(), 1);
961        assert_eq!(label.selection_end(), 3);
962        assert_eq!(*observed.borrow(), Some((1, 3)));
963        Application::unmount();
964    }
965
966    #[test]
967    fn runtime_text_state_stays_synchronized_without_user_callbacks() {
968        ffi::test::reset();
969        let label = TextNode::new("A你😀Z");
970        Application::mount(label.clone());
971        let replacement = "界";
972        unsafe {
973            crate::event::__fui_on_text_replaced(
974                label.handle().raw(),
975                1,
976                4,
977                replacement.as_ptr(),
978                replacement.len() as u32,
979            );
980        }
981        assert_eq!(label.content(), "A界😀Z");
982
983        let changed = "你好😀";
984        unsafe {
985            crate::event::__fui_on_text_changed(
986                label.handle().raw(),
987                changed.as_ptr(),
988                changed.len() as u32,
989            );
990        }
991        assert_eq!(label.content(), changed);
992        Application::unmount();
993    }
994}