Skip to main content

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