Skip to main content

floem/views/
rich_text.rs

1use std::{any::Any, cell::RefCell, rc::Rc};
2
3use floem_reactive::Effect;
4use floem_renderer::text::{Attrs, AttrsList, AttrsOwned};
5use peniko::{Color, color::palette};
6use smallvec::{SmallVec, smallvec};
7use taffy::tree::NodeId;
8
9use crate::{IntoView, context::UpdateCx, view::LayoutNodeCx, view::View, view::ViewId};
10
11use crate::text::TextLayoutState;
12
13pub struct RichText {
14    id: ViewId,
15    /// Layout data containing text layouts and overflow handling logic
16    layout_data: Rc<RefCell<TextLayoutState>>,
17    text_node: Option<NodeId>,
18    layout_node: Option<NodeId>,
19}
20
21pub fn rich_text(
22    text: String,
23    attrs_list: AttrsList,
24    text_fn: impl Fn() -> (String, AttrsList) + 'static,
25) -> RichText {
26    let id = ViewId::new();
27    Effect::new(move |_| {
28        let (new_text, new_attrs) = text_fn();
29        id.update_state((new_text, new_attrs));
30    });
31
32    let layout_data = Rc::new(RefCell::new(TextLayoutState::new(Some(id))));
33
34    // Initialize the layout data with the text and attrs
35    {
36        let mut data = layout_data.borrow_mut();
37        data.set_text(&text, attrs_list, None);
38        data.set_text_overflow(crate::style::TextOverflow::Wrap {
39            overflow_wrap: crate::text::OverflowWrap::Normal,
40            word_break: crate::text::WordBreakStrength::Normal,
41        });
42    }
43
44    let mut rich_text = RichText {
45        id,
46        layout_data,
47        text_node: None,
48        layout_node: None,
49    };
50
51    rich_text.set_taffy_layout();
52    rich_text
53}
54
55impl RichText {
56    fn mark_text_measure_dirty(&self) {
57        if let Some(text_node) = self.text_node {
58            let _ = self.id.taffy().borrow_mut().mark_dirty(text_node);
59        }
60        let _ = self.id.mark_view_layout_dirty();
61    }
62
63    fn set_taffy_layout(&mut self) {
64        let taffy_node = self.id.taffy_node();
65        let taffy = self.id.taffy();
66        let mut taffy = taffy.borrow_mut();
67        let text_node = taffy
68            .new_leaf(taffy::Style {
69                ..taffy::Style::DEFAULT
70            })
71            .unwrap();
72
73        let layout_fn = TextLayoutState::create_taffy_layout_fn(self.layout_data.clone());
74        let finalize_fn = TextLayoutState::create_finalize_fn(self.layout_data.clone());
75        self.text_node = Some(text_node);
76        self.layout_node = Some(taffy_node);
77
78        taffy
79            .set_node_context(
80                text_node,
81                Some(LayoutNodeCx::Custom {
82                    measure: layout_fn,
83                    finalize: Some(finalize_fn),
84                }),
85            )
86            .unwrap();
87        taffy.set_children(taffy_node, &[text_node]).unwrap();
88    }
89}
90
91impl View for RichText {
92    fn id(&self) -> ViewId {
93        self.id
94    }
95
96    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
97        self.layout_data
98            .borrow()
99            .get_text_layout()
100            .map(|_| {
101                let data = self.layout_data.borrow();
102                format!(
103                    "RichText: {:?}",
104                    crate::text::paragraph_ranges(data.text().unwrap_or_default())
105                        .map(|r| data.text().unwrap_or_default()[r].to_string())
106                        .collect::<Vec<_>>()
107                )
108            })
109            .unwrap_or_else(|| "RichText: <empty>".to_string())
110            .into()
111    }
112
113    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn Any>) {
114        if let Ok(state) = state.downcast::<(String, AttrsList)>() {
115            let (text, attrs_list) = *state;
116
117            let mut data = self.layout_data.borrow_mut();
118            data.set_text(&text, attrs_list, None);
119            data.set_text_overflow(crate::style::TextOverflow::Wrap {
120                overflow_wrap: crate::text::OverflowWrap::Normal,
121                word_break: crate::text::WordBreakStrength::Normal,
122            });
123            drop(data);
124
125            self.mark_text_measure_dirty();
126            self.id.request_layout();
127        }
128    }
129
130    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
131        let (Some(parent_node), Some(text_node)) = (self.layout_node, self.text_node) else {
132            return;
133        };
134
135        let text_loc = self
136            .id
137            .get_content_rect_relative(text_node, parent_node)
138            .unwrap_or_default()
139            .origin();
140
141        self.layout_data
142            .borrow()
143            .with_effective_text_layout(|layout| {
144                layout.draw(cx, text_loc);
145            });
146    }
147}
148
149#[derive(Clone, Debug)]
150pub struct RichSpan<'a> {
151    text: &'a str,
152    attrs: Attrs<'a>,
153}
154#[allow(clippy::wrong_self_convention)]
155impl<'a> RichSpan<'a> {
156    fn to_owned(self) -> RichSpanOwned {
157        let len = self.text.len();
158        RichSpanOwned {
159            text: self.text.to_string(),
160            spans: smallvec::smallvec![(0..len, AttrsOwned::new(self.attrs))],
161        }
162    }
163    pub fn color(mut self, color: Color) -> Self {
164        self.attrs = self.attrs.color(color);
165        self
166    }
167
168    pub fn family(mut self, family: &'a [floem_renderer::text::FamilyOwned]) -> RichSpan<'a> {
169        self.attrs = self.attrs.family(family);
170        self
171    }
172
173    pub fn font_width(mut self, stretch: floem_renderer::text::FontWidth) -> RichSpan<'a> {
174        self.attrs = self.attrs.font_width(stretch);
175        self
176    }
177
178    pub fn text_style(mut self, style: floem_renderer::text::FontStyle) -> RichSpan<'a> {
179        self.attrs = self.attrs.font_style(style);
180        self
181    }
182
183    pub fn weight(mut self, weight: floem_renderer::text::FontWeight) -> RichSpan<'a> {
184        self.attrs = self.attrs.weight(weight);
185        self
186    }
187
188    pub fn line_height(
189        mut self,
190        line_height: floem_renderer::text::LineHeightValue,
191    ) -> RichSpan<'a> {
192        self.attrs = self.attrs.line_height(line_height);
193        self
194    }
195
196    pub fn font_size(mut self, font_size: f32) -> RichSpan<'a> {
197        self.attrs = self.attrs.font_size(font_size);
198        self
199    }
200
201    pub fn raw_weight(mut self, weight: u16) -> RichSpan<'a> {
202        self.attrs = self.attrs.raw_weight(weight);
203        self
204    }
205}
206#[derive(Clone, Debug)]
207pub struct RichSpanOwned {
208    text: String,
209    spans: SmallVec<[(std::ops::Range<usize>, AttrsOwned); 3]>,
210}
211impl IntoView for RichSpanOwned {
212    type V = RichText;
213    type Intermediate = RichText;
214
215    fn into_intermediate(self) -> Self::Intermediate {
216        let mut attrs_list = AttrsList::new(Attrs::new().color(palette::css::BLACK));
217        for span in self.spans.clone() {
218            attrs_list.add_span(span.0, span.1.as_attrs());
219        }
220
221        let text = self.text.clone();
222        let text_clone = self.text.clone();
223        let spans = self.spans.clone();
224
225        rich_text(text, attrs_list, move || {
226            let mut attrs_list = AttrsList::new(Attrs::new().color(palette::css::BLACK));
227            for span in spans.clone() {
228                attrs_list.add_span(span.0, span.1.as_attrs());
229            }
230            (text_clone.clone(), attrs_list)
231        })
232    }
233}
234impl<'a> IntoView for RichSpan<'a> {
235    type V = RichText;
236    type Intermediate = RichText;
237
238    fn into_intermediate(self) -> Self::Intermediate {
239        self.to_owned().into_intermediate()
240    }
241}
242impl<'a, S> std::ops::Add<S> for RichSpan<'a>
243where
244    RichSpan<'a>: From<S>,
245{
246    type Output = RichSpanOwned;
247
248    fn add(self, rhs: S) -> Self::Output {
249        let self_len = self.text.len();
250        let rhs: RichSpan = rhs.into();
251        let rhs_len = rhs.text.len();
252        RichSpanOwned {
253            text: self.text.to_string() + rhs.text,
254            spans: smallvec![
255                (0..self_len, AttrsOwned::new(self.attrs)),
256                (self_len..self_len + rhs_len, AttrsOwned::new(rhs.attrs)),
257            ],
258        }
259    }
260}
261impl<'a> std::ops::Add<&'a str> for RichSpan<'a> {
262    type Output = RichSpanOwned;
263
264    fn add(self, rhs: &'a str) -> Self::Output {
265        let self_len = self.text.len();
266        let rhs_len = rhs.len();
267        RichSpanOwned {
268            text: self.text.to_string() + rhs,
269            spans: smallvec![
270                (0..self_len, AttrsOwned::new(self.attrs)),
271                (
272                    self_len..self_len + rhs_len,
273                    AttrsOwned::new(Attrs::new().color(palette::css::BLACK))
274                ),
275            ],
276        }
277    }
278}
279impl std::ops::Add<String> for RichSpan<'_> {
280    type Output = RichSpanOwned;
281
282    fn add(self, rhs: String) -> Self::Output {
283        let self_len = self.text.len();
284        let rhs_len = rhs.len();
285        RichSpanOwned {
286            text: self.text.to_string() + &rhs,
287            spans: smallvec![
288                (0..self_len, AttrsOwned::new(self.attrs)),
289                (
290                    self_len..self_len + rhs_len,
291                    AttrsOwned::new(Attrs::new().color(palette::css::BLACK))
292                ),
293            ],
294        }
295    }
296}
297impl<'a, S> std::ops::Add<S> for RichSpanOwned
298where
299    RichSpan<'a>: From<S>,
300{
301    type Output = Self;
302
303    fn add(mut self, rhs: S) -> Self::Output {
304        let rhs: RichSpan = rhs.into();
305        let self_len = self.text.len();
306        let new_text = self.text + rhs.text;
307        self.spans
308            .push((self_len..new_text.len(), AttrsOwned::new(rhs.attrs)));
309        Self {
310            text: new_text,
311            spans: self.spans,
312        }
313    }
314}
315impl std::ops::Add<&str> for RichSpanOwned {
316    type Output = RichSpanOwned;
317
318    fn add(mut self, rhs: &str) -> Self::Output {
319        let self_len = self.text.len();
320        let new_text = self.text + rhs;
321        self.spans.push((
322            self_len..new_text.len(),
323            AttrsOwned::new(Attrs::new().color(palette::css::BLACK)),
324        ));
325        Self {
326            text: new_text,
327            spans: self.spans,
328        }
329    }
330}
331impl std::ops::Add<String> for RichSpanOwned {
332    type Output = RichSpanOwned;
333
334    fn add(mut self, rhs: String) -> Self::Output {
335        let self_len = self.text.len();
336        let new_text = self.text + &rhs;
337        self.spans.push((
338            self_len..new_text.len(),
339            AttrsOwned::new(Attrs::new().color(palette::css::BLACK)),
340        ));
341        Self {
342            text: new_text,
343            spans: self.spans,
344        }
345    }
346}
347impl std::ops::Add for RichSpanOwned {
348    type Output = Self;
349
350    fn add(mut self, rhs: Self) -> Self::Output {
351        let self_len = self.text.len();
352        self.spans.extend(
353            rhs.spans
354                .into_iter()
355                .map(|span| ((span.0.start + self_len)..(span.0.end + self_len), span.1)),
356        );
357        Self {
358            text: self.text + &rhs.text,
359            spans: self.spans,
360        }
361    }
362}
363
364pub trait RichTextExt<'a>
365where
366    Self: Sized,
367    RichSpan<'a>: From<Self>,
368{
369    fn color(self, color: Color) -> RichSpan<'a> {
370        let span: RichSpan = self.into();
371        span.color(color)
372    }
373    fn red(self) -> RichSpan<'a> {
374        self.color(palette::css::RED)
375    }
376    fn blue(self) -> RichSpan<'a> {
377        self.color(palette::css::BLUE)
378    }
379
380    fn green(self) -> RichSpan<'a> {
381        self.color(palette::css::GREEN)
382    }
383
384    fn yellow(self) -> RichSpan<'a> {
385        self.color(palette::css::YELLOW)
386    }
387
388    fn black(self) -> RichSpan<'a> {
389        self.color(palette::css::BLACK)
390    }
391
392    fn white(self) -> RichSpan<'a> {
393        self.color(palette::css::WHITE)
394    }
395
396    fn gray(self) -> RichSpan<'a> {
397        self.color(palette::css::GRAY)
398    }
399
400    fn cyan(self) -> RichSpan<'a> {
401        self.color(palette::css::CYAN)
402    }
403
404    fn magenta(self) -> RichSpan<'a> {
405        self.color(palette::css::MAGENTA)
406    }
407
408    fn orange(self) -> RichSpan<'a> {
409        self.color(palette::css::ORANGE)
410    }
411
412    fn purple(self) -> RichSpan<'a> {
413        self.color(palette::css::PURPLE)
414    }
415
416    fn pink(self) -> RichSpan<'a> {
417        self.color(palette::css::PINK)
418    }
419
420    fn family(self, family: &'a [crate::text::FamilyOwned]) -> RichSpan<'a> {
421        let span: RichSpan = self.into();
422        span.family(family)
423    }
424    fn stretch(self, stretch: crate::text::FontWidth) -> RichSpan<'a> {
425        let span: RichSpan = self.into();
426        span.font_width(stretch)
427    }
428    fn text_style(self, style: crate::text::FontStyle) -> RichSpan<'a> {
429        let span: RichSpan = self.into();
430        span.text_style(style)
431    }
432    fn italic(self) -> RichSpan<'a> {
433        self.text_style(crate::text::FontStyle::Italic)
434    }
435    fn oblique(self) -> RichSpan<'a> {
436        self.text_style(crate::text::FontStyle::Oblique(None))
437    }
438
439    fn weight(self, weight: crate::text::FontWeight) -> RichSpan<'a> {
440        let span: RichSpan = self.into();
441        span.weight(weight)
442    }
443    fn thin(self) -> RichSpan<'a> {
444        self.weight(crate::text::FontWeight::THIN)
445    }
446    fn extra_light(self) -> RichSpan<'a> {
447        self.weight(crate::text::FontWeight::EXTRA_LIGHT)
448    }
449    fn light(self) -> RichSpan<'a> {
450        self.weight(crate::text::FontWeight::LIGHT)
451    }
452    fn medium(self) -> RichSpan<'a> {
453        self.weight(crate::text::FontWeight::MEDIUM)
454    }
455    fn semibold(self) -> RichSpan<'a> {
456        self.weight(crate::text::FontWeight::SEMI_BOLD)
457    }
458    fn bold(self) -> RichSpan<'a> {
459        self.weight(crate::text::FontWeight::BOLD)
460    }
461    fn extra_bold(self) -> RichSpan<'a> {
462        self.weight(crate::text::FontWeight::EXTRA_BOLD)
463    }
464
465    fn raw_weight(self, weight: u16) -> RichSpan<'a> {
466        let span: RichSpan = self.into();
467        span.raw_weight(weight)
468    }
469    fn font_size(self, font_size: f32) -> RichSpan<'a> {
470        let span: RichSpan = self.into();
471        span.font_size(font_size)
472    }
473
474    fn line_height(self, line_height: crate::text::LineHeightValue) -> RichSpan<'a> {
475        let span: RichSpan = self.into();
476        span.line_height(line_height)
477    }
478}
479
480impl<'a, S> RichTextExt<'a> for S
481where
482    S: AsRef<str>,
483    RichSpan<'a>: From<S>,
484{
485}
486impl<'a, S: AsRef<str> + 'a> From<&'a S> for RichSpan<'a> {
487    fn from(value: &'a S) -> Self {
488        RichSpan {
489            text: value.as_ref(),
490            attrs: Attrs::new().color(palette::css::BLACK),
491        }
492    }
493}
494impl<'a> RichTextExt<'a> for RichSpan<'a> {}