floem/views/
rich_text.rs

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