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