iced_widget/text/
rich.rs

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