Skip to main content

iced_widget/text/
rich.rs

1use crate::core::alignment;
2use crate::core::border;
3use crate::core::layout;
4use crate::core::mouse;
5use crate::core::renderer;
6use crate::core::text::{Paragraph, Span};
7use crate::core::widget::text::{
8    self, Alignment, Catalog, Ellipsis, LineHeight, Shaping, Style, StyleFn, Wrapping,
9};
10use crate::core::widget::tree::{self, Tree};
11use crate::core::{
12    self, Border, Color, Element, Event, Font, Layout, Length, Pixels, Point, Rectangle, Shell,
13    Size, Vector, Widget,
14};
15
16/// A bunch of [`Rich`] text.
17pub struct Rich<'a, Link, Message, Theme = crate::Theme>
18where
19    Link: Clone + 'static,
20    Theme: Catalog,
21{
22    spans: Box<dyn AsRef<[Span<'a, Link>]> + 'a>,
23    size: Option<Pixels>,
24    line_height: Option<LineHeight>,
25    width: Length,
26    height: Length,
27    font: Option<Font>,
28    align_x: Alignment,
29    align_y: alignment::Vertical,
30    wrapping: Wrapping,
31    ellipsis: Ellipsis,
32    class: Theme::Class<'a>,
33    hovered_link: Option<usize>,
34    on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
35}
36
37impl<'a, Link, Message, Theme> Rich<'a, Link, Message, Theme>
38where
39    Link: Clone + 'static,
40    Theme: Catalog,
41{
42    /// Creates a new empty [`Rich`] text.
43    pub fn new() -> Self {
44        Self {
45            spans: Box::new([]),
46            size: None,
47            line_height: None,
48            width: Length::Shrink,
49            height: Length::Shrink,
50            font: None,
51            align_x: Alignment::Default,
52            align_y: alignment::Vertical::Top,
53            wrapping: Wrapping::default(),
54            ellipsis: Ellipsis::default(),
55            class: Theme::default(),
56            hovered_link: None,
57            on_link_click: None,
58        }
59    }
60
61    /// Creates a new [`Rich`] text with the given text spans.
62    pub fn with_spans(spans: impl AsRef<[Span<'a, Link>]> + 'a) -> Self {
63        Self {
64            spans: Box::new(spans),
65            ..Self::new()
66        }
67    }
68
69    /// Sets the default size of the [`Rich`] text.
70    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
71        self.size = Some(size.into());
72        self
73    }
74
75    /// Sets the default [`LineHeight`] of the [`Rich`] text.
76    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
77        self.line_height = Some(line_height.into());
78        self
79    }
80
81    /// Sets the default font of the [`Rich`] text.
82    pub fn font(mut self, font: impl Into<Font>) -> Self {
83        self.font = Some(font.into());
84        self
85    }
86
87    /// Sets the width of the [`Rich`] text boundaries.
88    pub fn width(mut self, width: impl Into<Length>) -> Self {
89        self.width = width.into();
90        self
91    }
92
93    /// Sets the height of the [`Rich`] text boundaries.
94    pub fn height(mut self, height: impl Into<Length>) -> Self {
95        self.height = height.into();
96        self
97    }
98
99    /// Centers the [`Rich`] text, both horizontally and vertically.
100    pub fn center(self) -> Self {
101        self.align_x(alignment::Horizontal::Center)
102            .align_y(alignment::Vertical::Center)
103    }
104
105    /// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
106    pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
107        self.align_x = alignment.into();
108        self
109    }
110
111    /// Sets the [`alignment::Vertical`] of the [`Rich`] text.
112    pub fn align_y(mut self, alignment: impl Into<alignment::Vertical>) -> Self {
113        self.align_y = alignment.into();
114        self
115    }
116
117    /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
118    pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
119        self.wrapping = wrapping;
120        self
121    }
122
123    /// Sets the [`Ellipsis`] strategy of the [`Rich`] text.
124    pub fn ellipsis(mut self, ellipsis: Ellipsis) -> Self {
125        self.ellipsis = ellipsis;
126        self
127    }
128
129    /// Sets the message that will be produced when a link of the [`Rich`] text
130    /// is clicked.
131    ///
132    /// If the spans of the [`Rich`] text contain no links, you may need to call
133    /// this method with `on_link_click(never)` in order for the compiler to infer
134    /// the proper `Link` generic type.
135    pub fn on_link_click(mut self, on_link_click: impl Fn(Link) -> Message + 'a) -> Self {
136        self.on_link_click = Some(Box::new(on_link_click));
137        self
138    }
139
140    /// Sets the default style of the [`Rich`] text.
141    #[must_use]
142    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
143    where
144        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
145    {
146        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
147        self
148    }
149
150    /// Sets the default [`Color`] of the [`Rich`] text.
151    pub fn color(self, color: impl Into<Color>) -> Self
152    where
153        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
154    {
155        self.color_maybe(Some(color))
156    }
157
158    /// Sets the default [`Color`] of the [`Rich`] text, if `Some`.
159    pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
160    where
161        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
162    {
163        let color = color.map(Into::into);
164
165        self.style(move |_theme| Style { color })
166    }
167
168    /// Sets the default style class of the [`Rich`] text.
169    #[cfg(feature = "advanced")]
170    #[must_use]
171    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
172        self.class = class.into();
173        self
174    }
175}
176
177impl<'a, Link, Message, Theme> Default for Rich<'a, Link, Message, Theme>
178where
179    Link: Clone + 'a,
180    Theme: Catalog,
181{
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187struct State<Link, P: Paragraph> {
188    spans: Vec<Span<'static, Link>>,
189    span_pressed: Option<usize>,
190    paragraph: P,
191}
192
193impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
194    for Rich<'_, Link, Message, Theme>
195where
196    Link: Clone + 'static,
197    Theme: Catalog,
198    Renderer: core::text::Renderer,
199{
200    fn tag(&self) -> tree::Tag {
201        tree::Tag::of::<State<Link, Renderer::Paragraph>>()
202    }
203
204    fn state(&self) -> tree::State {
205        tree::State::new(State::<Link, _> {
206            spans: Vec::new(),
207            span_pressed: None,
208            paragraph: Renderer::Paragraph::default(),
209        })
210    }
211
212    fn size(&self) -> Size<Length> {
213        Size {
214            width: self.width,
215            height: self.height,
216        }
217    }
218
219    fn layout(
220        &mut self,
221        tree: &mut Tree,
222        renderer: &Renderer,
223        limits: &layout::Limits,
224    ) -> layout::Node {
225        layout(
226            tree.state
227                .downcast_mut::<State<Link, Renderer::Paragraph>>(),
228            renderer,
229            limits,
230            self.width,
231            self.height,
232            self.spans.as_ref().as_ref(),
233            self.line_height,
234            self.size,
235            self.font,
236            self.align_x,
237            self.align_y,
238            self.wrapping,
239            self.ellipsis,
240        )
241    }
242
243    fn draw(
244        &self,
245        tree: &Tree,
246        renderer: &mut Renderer,
247        theme: &Theme,
248        defaults: &renderer::Style,
249        layout: Layout<'_>,
250        _cursor: mouse::Cursor,
251        viewport: &Rectangle,
252    ) {
253        if !layout.bounds().intersects(viewport) {
254            return;
255        }
256
257        let state = tree
258            .state
259            .downcast_ref::<State<Link, Renderer::Paragraph>>();
260
261        let style = theme.style(&self.class);
262
263        for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
264            let is_hovered_link = self.on_link_click.is_some() && Some(index) == self.hovered_link;
265
266            if span.highlight.is_some() || span.underline || span.strikethrough || is_hovered_link {
267                let translation = layout.position() - Point::ORIGIN;
268                let regions = state.paragraph.span_bounds(index);
269
270                if let Some(highlight) = span.highlight {
271                    for (i, bounds) in regions.iter().enumerate() {
272                        let starts = i == 0;
273                        let ends = i + 1 == regions.len();
274
275                        // The horizontal padding belongs to the start and end
276                        // of the span, not to each of its lines
277                        let left = if starts { span.padding.left } else { 0.0 };
278                        let right = if ends { span.padding.right } else { 0.0 };
279
280                        let bounds = Rectangle::new(
281                            bounds.position() - Vector::new(left, span.padding.top),
282                            bounds.size() + Size::new(left + right, span.padding.y()),
283                        );
284
285                        let radius = border::Radius {
286                            top_left: highlight.border.radius.top_left * f32::from(starts),
287                            bottom_left: highlight.border.radius.bottom_left * f32::from(starts),
288                            top_right: highlight.border.radius.top_right * f32::from(ends),
289                            bottom_right: highlight.border.radius.bottom_right * f32::from(ends),
290                        };
291
292                        renderer.fill_quad(
293                            renderer::Quad {
294                                bounds: bounds + translation,
295                                border: Border {
296                                    radius,
297                                    ..highlight.border
298                                },
299                                ..Default::default()
300                            },
301                            highlight.background,
302                        );
303                    }
304                }
305
306                if span.underline || span.strikethrough || is_hovered_link {
307                    let size = span.size.or(self.size).unwrap_or(renderer.text_size());
308
309                    let line_height = span
310                        .line_height
311                        .or(self.line_height)
312                        .unwrap_or_else(|| renderer.line_height())
313                        .to_absolute(size);
314
315                    let color = span.color.or(style.color).unwrap_or(defaults.text_color);
316
317                    let baseline =
318                        translation + Vector::new(0.0, size.0 + (line_height.0 - size.0) / 2.0);
319
320                    if span.underline || is_hovered_link {
321                        for bounds in &regions {
322                            renderer.fill_quad(
323                                renderer::Quad {
324                                    bounds: Rectangle::new(
325                                        bounds.position() + baseline,
326                                        Size::new(bounds.width, 1.0),
327                                    ),
328                                    ..Default::default()
329                                },
330                                color,
331                            );
332                        }
333                    }
334
335                    if span.strikethrough {
336                        for bounds in &regions {
337                            renderer.fill_quad(
338                                renderer::Quad {
339                                    bounds: Rectangle::new(
340                                        bounds.position() + baseline
341                                            - Vector::new(0.0, size.0 / 2.0),
342                                        Size::new(bounds.width, 1.0),
343                                    ),
344                                    ..Default::default()
345                                },
346                                color,
347                            );
348                        }
349                    }
350                }
351            }
352        }
353
354        text::draw(
355            renderer,
356            defaults,
357            layout.bounds(),
358            &state.paragraph,
359            style,
360            viewport,
361        );
362    }
363
364    fn update(
365        &mut self,
366        tree: &mut Tree,
367        event: &Event,
368        layout: Layout<'_>,
369        cursor: mouse::Cursor,
370        _renderer: &Renderer,
371        shell: &mut Shell<'_, Message>,
372        _viewport: &Rectangle,
373    ) {
374        let Some(on_link_clicked) = &self.on_link_click else {
375            return;
376        };
377
378        let was_hovered = self.hovered_link.is_some();
379
380        if let Some(position) = cursor.position_in(layout.bounds()) {
381            let state = tree
382                .state
383                .downcast_ref::<State<Link, Renderer::Paragraph>>();
384
385            self.hovered_link = state.paragraph.hit_span(position).and_then(|span| {
386                if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
387                    Some(span)
388                } else {
389                    None
390                }
391            });
392        } else {
393            self.hovered_link = None;
394        }
395
396        if was_hovered != self.hovered_link.is_some() {
397            shell.request_redraw();
398        }
399
400        match event {
401            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
402                let state = tree
403                    .state
404                    .downcast_mut::<State<Link, Renderer::Paragraph>>();
405
406                if self.hovered_link.is_some() {
407                    state.span_pressed = self.hovered_link;
408                    shell.capture_event();
409                }
410            }
411            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
412                let state = tree
413                    .state
414                    .downcast_mut::<State<Link, Renderer::Paragraph>>();
415
416                match state.span_pressed {
417                    Some(span) if Some(span) == self.hovered_link => {
418                        if let Some(link) = self
419                            .spans
420                            .as_ref()
421                            .as_ref()
422                            .get(span)
423                            .and_then(|span| span.link.clone())
424                        {
425                            shell.publish(on_link_clicked(link));
426                        }
427                    }
428                    _ => {}
429                }
430
431                state.span_pressed = None;
432            }
433            _ => {}
434        }
435    }
436
437    fn mouse_interaction(
438        &self,
439        _tree: &Tree,
440        _layout: Layout<'_>,
441        _cursor: mouse::Cursor,
442        _viewport: &Rectangle,
443        _renderer: &Renderer,
444    ) -> mouse::Interaction {
445        if self.hovered_link.is_some() {
446            mouse::Interaction::Pointer
447        } else {
448            mouse::Interaction::None
449        }
450    }
451}
452
453fn layout<Link, Renderer>(
454    state: &mut State<Link, Renderer::Paragraph>,
455    renderer: &Renderer,
456    limits: &layout::Limits,
457    width: Length,
458    height: Length,
459    spans: &[Span<'_, Link>],
460    line_height: Option<LineHeight>,
461    size: Option<Pixels>,
462    font: Option<Font>,
463    align_x: Alignment,
464    align_y: alignment::Vertical,
465    wrapping: Wrapping,
466    ellipsis: Ellipsis,
467) -> layout::Node
468where
469    Link: Clone,
470    Renderer: core::text::Renderer,
471{
472    layout::sized(limits, width, height, |limits| {
473        let bounds = limits.max();
474
475        let size = size.unwrap_or_else(|| renderer.text_size());
476        let font = font.unwrap_or_else(|| renderer.font());
477        let line_height = line_height.unwrap_or_else(|| renderer.line_height());
478
479        let text_with_spans = || core::Text {
480            content: spans,
481            bounds,
482            size,
483            line_height,
484            font,
485            align_x,
486            align_y,
487            shaping: Shaping::Advanced,
488            wrapping,
489            ellipsis,
490            hint_factor: renderer.hint_factor(),
491        };
492
493        if state.spans != spans {
494            state.paragraph = Renderer::Paragraph::with_spans(text_with_spans());
495            state.spans = spans.iter().cloned().map(Span::to_static).collect();
496        } else {
497            match state.paragraph.compare(core::Text {
498                content: (),
499                bounds,
500                size,
501                line_height,
502                font,
503                align_x,
504                align_y,
505                shaping: Shaping::Advanced,
506                wrapping,
507                ellipsis,
508                hint_factor: renderer.hint_factor(),
509            }) {
510                core::text::Difference::None => {}
511                core::text::Difference::Bounds => {
512                    state.paragraph.resize(bounds);
513                }
514                core::text::Difference::Shape => {
515                    state.paragraph = Renderer::Paragraph::with_spans(text_with_spans());
516                }
517            }
518        }
519
520        state.paragraph.min_bounds()
521    })
522}
523
524impl<'a, Link, Message, Theme> FromIterator<Span<'a, Link>> for Rich<'a, Link, Message, Theme>
525where
526    Link: Clone + 'a,
527    Theme: Catalog,
528{
529    fn from_iter<T: IntoIterator<Item = Span<'a, Link>>>(spans: T) -> Self {
530        Self::with_spans(spans.into_iter().collect::<Vec<_>>())
531    }
532}
533
534impl<'a, Link, Message, Theme, Renderer> From<Rich<'a, Link, Message, Theme>>
535    for Element<'a, Message, Theme, Renderer>
536where
537    Message: 'a,
538    Link: Clone + 'a,
539    Theme: Catalog + 'a,
540    Renderer: core::text::Renderer + 'a,
541{
542    fn from(text: Rich<'a, Link, Message, Theme>) -> Element<'a, Message, Theme, Renderer> {
543        Element::new(text)
544    }
545}