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