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(¤t, byte_to_scalar(¤t, start));
644 let end_byte =
645 scalar_to_byte(¤t, byte_to_scalar(¤t, 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 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}