iced_widget/text/
rich.rs

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