iced_selection/
text.rs

1//! Text widgets display information through writing.
2//!
3//! Keyboard shortcuts (applies to both [`Text`] and [`Rich`]):
4//!
5//! |MacOS|Linux/Windows|Effect|
6//! |-|-|-|
7//! |`Cmd + A`|`Ctrl + A`|Selects all text in the currently focused paragraph|
8//! |`Cmd + C`|`Ctrl + C`|Copies the selected text to clipboard|
9//! |`Shift + Left Arrow`|`Shift + Left Arrow`|Moves the selection to the left by one character|
10//! |`Shift + Right Arrow`|`Shift + Right Arrow`|Moves the selection to the right by one character|
11//! |`Shift + Opt + Left Arrow`|`Shift + Ctrl + Left Arrow`|Extends the selection to the previous start of a word|
12//! |`Shift + Opt + Right Arrow`|`Shift + Ctrl + Right Arrow`|Extends the selection to the next end of a word|
13//! |`Shift + Home`<br>`Shift + Cmd + Left Arrow`<br>`Shift + Opt + Up Arrow`|`Shift + Home`<br>`Shift + Ctrl + Up Arrow`|Selects to the beginning of the line|
14//! |`Shift + End`<br>`Shift + Cmd + Right Arrow`<br>`Shift + Opt + Down Arrow`|`Shift + End`<br>`Shift + Ctrl + Down Arrow`|Selects to the end of the line|
15//! |`Shift + Up Arrow`|`Shift + Up Arrow`|Moves the selection up by one line if possible, or to the start of the current line otherwise|
16//! |`Shift + Down Arrow`|`Shift + Down Arrow`|Moves the selection down by one line if possible, or to the end of the current line otherwise|
17//! |`Shift + Opt + Home`<br>`Shift + Cmd + Up Arrow`|`Shift + Ctrl + Home`|Selects to the beginning of the paragraph|
18//! |`Shift + Opt + End`<br>`Shift + Cmd + Down Arrow`|`Shift + Ctrl + End`|Selects to the end of the paragraph|
19mod rich;
20
21use iced_widget::graphics::text::Paragraph;
22pub use rich::Rich;
23use text::{Alignment, LineHeight, Shaping, Wrapping};
24pub use text::{Fragment, Highlighter, IntoFragment, Span};
25
26use crate::core::alignment;
27use crate::core::clipboard;
28use crate::core::keyboard::{self, key};
29use crate::core::layout;
30use crate::core::mouse;
31use crate::core::mouse::click;
32use crate::core::renderer;
33use crate::core::text;
34use crate::core::text::paragraph::Paragraph as _;
35use crate::core::touch;
36use crate::core::widget::Operation;
37use crate::core::widget::text::Format;
38use crate::core::widget::tree::{self, Tree};
39use crate::core::{
40    self, Color, Element, Event, Font, Layout, Length, Pixels, Point, Size,
41    Theme, Widget,
42};
43use crate::selection::{Selection, SelectionEnd};
44
45/// A bunch of text.
46///
47/// # Example
48/// ```no_run,ignore
49/// use iced_selection::text;
50///
51/// enum Message {
52///     // ...
53/// }
54///
55/// fn view(state: &State) -> Element<'_, Message> {
56///     text("Hello, this is iced!")
57///         .size(20)
58///         .into()
59/// }
60/// ```
61pub struct Text<
62    'a,
63    Theme = iced_widget::Theme,
64    Renderer = iced_widget::Renderer,
65> where
66    Theme: Catalog,
67    Renderer: text::Renderer,
68{
69    fragment: Fragment<'a>,
70    format: Format<Renderer::Font>,
71    class: Theme::Class<'a>,
72}
73
74impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
75where
76    Theme: Catalog,
77    Renderer: text::Renderer,
78{
79    /// Create a new fragment of [`Text`] with the given contents.
80    pub fn new(fragment: impl IntoFragment<'a>) -> Self {
81        Self {
82            fragment: fragment.into_fragment(),
83            format: Format::default(),
84            class: Theme::default(),
85        }
86    }
87
88    /// Sets the size of the [`Text`].
89    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
90        self.format.size = Some(size.into());
91        self
92    }
93
94    /// Sets the [`LineHeight`] of the [`Text`].
95    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
96        self.format.line_height = line_height.into();
97        self
98    }
99
100    /// Sets the [`Font`] of the [`Text`].
101    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
102        self.format.font = Some(font.into());
103        self
104    }
105
106    /// Sets the width of the [`Text`] boundaries.
107    pub fn width(mut self, width: impl Into<Length>) -> Self {
108        self.format.width = width.into();
109        self
110    }
111
112    /// Sets the height of the [`Text`] boundaries.
113    pub fn height(mut self, height: impl Into<Length>) -> Self {
114        self.format.height = height.into();
115        self
116    }
117
118    /// Centers the [`Text`], both horizontally and vertically.
119    pub fn center(mut self) -> Self {
120        self.format.align_x = Alignment::Center;
121        self.format.align_y = alignment::Vertical::Center;
122        self
123    }
124
125    /// Sets the [`alignment::Horizontal`] of the [`Text`].
126    pub fn align_x(mut self, alignment: impl Into<text::Alignment>) -> Self {
127        self.format.align_x = alignment.into();
128        self
129    }
130
131    /// Sets the [`alignment::Vertical`] of the [`Text`].
132    pub fn align_y(
133        mut self,
134        alignment: impl Into<alignment::Vertical>,
135    ) -> Self {
136        self.format.align_y = alignment.into();
137        self
138    }
139
140    /// Sets the [`Shaping`] strategy of the [`Text`].
141    pub fn shaping(mut self, shaping: Shaping) -> Self {
142        self.format.shaping = shaping;
143        self
144    }
145
146    /// Sets the [`Wrapping`] strategy of the [`Text`].
147    pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
148        self.format.wrapping = wrapping;
149        self
150    }
151
152    /// Sets the style of the [`Text`].
153    #[must_use]
154    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
155    where
156        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
157    {
158        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
159        self
160    }
161
162    /// Sets the style class of the [`Text`].
163    #[must_use]
164    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
165        self.class = class.into();
166        self
167    }
168}
169
170/// The internal state of a [`Text`] widget.
171#[derive(Debug, Default, Clone)]
172pub struct State {
173    paragraph: Paragraph,
174    content: String,
175    is_hovered: bool,
176    selection: Selection,
177    dragging: Option<Dragging>,
178    last_click: Option<mouse::Click>,
179    keyboard_modifiers: keyboard::Modifiers,
180}
181
182/// The type of dragging selection.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184#[allow(missing_docs)]
185pub enum Dragging {
186    Grapheme,
187    Word,
188    Line,
189}
190
191impl State {
192    fn grapheme_line_and_index(&self, point: Point) -> Option<(usize, usize)> {
193        let cursor = self.paragraph.buffer().hit(point.x, point.y)?;
194        let value = self.paragraph.buffer().lines[cursor.line].text();
195
196        Some((
197            cursor.line,
198            unicode_segmentation::UnicodeSegmentation::graphemes(
199                &value[..cursor.index.min(value.len())],
200                true,
201            )
202            .count(),
203        ))
204    }
205
206    fn selection_end_points(&self) -> (usize, Point, Point) {
207        let Selection { start, end, .. } = self.selection;
208
209        let (start_row, start_position) = self
210            .grapheme_position(start.line, start.index)
211            .unwrap_or_default();
212
213        let (end_row, end_position) = self
214            .grapheme_position(end.line, end.index)
215            .unwrap_or_default();
216
217        (
218            end_row.saturating_sub(start_row) + 1,
219            start_position,
220            end_position,
221        )
222    }
223
224    fn grapheme_position(
225        &self,
226        line: usize,
227        index: usize,
228    ) -> Option<(usize, Point)> {
229        use unicode_segmentation::UnicodeSegmentation;
230
231        let mut first_run_index = None;
232        let mut last_run_index = None;
233        let mut last_start = None;
234        let mut last_grapheme_count = 0;
235        let mut last_run_graphemes = 0;
236        let mut real_index = 0;
237        let mut graphemes_seen = 0;
238
239        let mut glyphs = self
240            .paragraph
241            .buffer()
242            .layout_runs()
243            .enumerate()
244            .filter(|(_, run)| run.line_i == line)
245            .flat_map(|(run_idx, run)| {
246                let line_top = run.line_top;
247
248                if first_run_index.is_none() {
249                    first_run_index = Some(run_idx);
250                }
251
252                run.glyphs.iter().map(move |glyph| {
253                    let mut glyph = glyph.clone();
254                    glyph.y += line_top;
255                    (run_idx, glyph, run.text)
256                })
257            });
258
259        let (_, glyph, _) = glyphs
260            .find(|(run_idx, glyph, text)| {
261                if Some(glyph.start) != last_start {
262                    last_grapheme_count =
263                        text[glyph.start..glyph.end].graphemes(false).count();
264                    last_start = Some(glyph.start);
265                    graphemes_seen += last_grapheme_count;
266                    last_run_graphemes += last_grapheme_count;
267                    real_index += last_grapheme_count;
268
269                    if Some(*run_idx) != last_run_index
270                        && graphemes_seen < index
271                    {
272                        real_index = last_grapheme_count;
273                        last_run_graphemes = last_grapheme_count;
274                    }
275                } else if Some(*run_idx) != last_run_index
276                    && graphemes_seen < index
277                {
278                    real_index = 0;
279                    last_run_graphemes = 0;
280                }
281
282                last_run_index = Some(*run_idx);
283
284                graphemes_seen >= index
285            })
286            .or_else(|| glyphs.last())?;
287
288        real_index -= graphemes_seen.saturating_sub(index);
289        real_index =
290            real_index.saturating_sub(last_run_index? - first_run_index?);
291
292        last_run_graphemes = last_run_graphemes
293            .saturating_sub(last_run_index? - first_run_index?);
294
295        let advance = if last_run_index? - first_run_index? <= 1 {
296            if real_index == 0 {
297                0.0
298            } else {
299                glyph.w
300                    * (1.0
301                        - last_run_graphemes.saturating_sub(real_index) as f32
302                            / last_grapheme_count.max(1) as f32)
303                    - glyph.w * (last_run_index? - first_run_index?) as f32
304            }
305        } else {
306            -(glyph.w
307                * (1.0
308                    + last_run_graphemes.saturating_sub(real_index) as f32
309                        / last_grapheme_count.max(1) as f32))
310        };
311
312        Some((
313            last_run_index?,
314            Point::new(
315                glyph.x + glyph.x_offset * glyph.font_size + advance,
316                glyph.y - glyph.y_offset * glyph.font_size,
317            ),
318        ))
319    }
320
321    fn update(&mut self, text: text::Text<&str, Font>) {
322        if self.content != text.content {
323            text.content.clone_into(&mut self.content);
324            self.paragraph = Paragraph::with_text(text);
325            return;
326        }
327
328        match self.paragraph.compare(text.with_content(())) {
329            text::Difference::None => {}
330            text::Difference::Bounds => self.paragraph.resize(text.bounds),
331            text::Difference::Shape => {
332                self.paragraph = Paragraph::with_text(text);
333            }
334        }
335    }
336}
337
338impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
339    for Text<'_, Theme, Renderer>
340where
341    Theme: Catalog,
342    Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
343{
344    fn tag(&self) -> tree::Tag {
345        tree::Tag::of::<State>()
346    }
347
348    fn state(&self) -> tree::State {
349        tree::State::new(State::default())
350    }
351
352    fn size(&self) -> Size<Length> {
353        Size {
354            width: self.format.width,
355            height: self.format.height,
356        }
357    }
358
359    fn layout(
360        &mut self,
361        tree: &mut Tree,
362        renderer: &Renderer,
363        limits: &layout::Limits,
364    ) -> layout::Node {
365        layout(
366            tree.state.downcast_mut::<State>(),
367            renderer,
368            limits,
369            &self.fragment,
370            self.format,
371        )
372    }
373
374    fn update(
375        &mut self,
376        tree: &mut Tree,
377        event: &Event,
378        layout: Layout<'_>,
379        cursor: mouse::Cursor,
380        _renderer: &Renderer,
381        clipboard: &mut dyn core::Clipboard,
382        shell: &mut core::Shell<'_, Message>,
383        viewport: &core::Rectangle,
384    ) {
385        let state = tree.state.downcast_mut::<State>();
386
387        let bounds = layout.bounds();
388        let click_position = cursor.position_in(bounds);
389
390        if viewport.intersection(&bounds).is_none()
391            && state.selection.is_empty()
392            && state.dragging.is_none()
393        {
394            return;
395        }
396
397        let was_hovered = state.is_hovered;
398        let selection_before = state.selection;
399        state.is_hovered = click_position.is_some();
400
401        match event {
402            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
403            | Event::Touch(touch::Event::FingerPressed { .. }) => {
404                if let Some(position) = click_position {
405                    let click = mouse::Click::new(
406                        position,
407                        mouse::Button::Left,
408                        state.last_click,
409                    );
410
411                    let (line, index) = state
412                        .grapheme_line_and_index(position)
413                        .unwrap_or((0, 0));
414
415                    match click.kind() {
416                        click::Kind::Single => {
417                            let new_end = SelectionEnd { line, index };
418
419                            if state.keyboard_modifiers.shift() {
420                                state.selection.change_selection(new_end);
421                            } else {
422                                state.selection.select_range(new_end, new_end);
423                            }
424
425                            state.dragging = Some(Dragging::Grapheme);
426                        }
427                        click::Kind::Double => {
428                            state.selection.select_word(
429                                line,
430                                index,
431                                &state.paragraph,
432                            );
433
434                            state.dragging = Some(Dragging::Word);
435                        }
436                        click::Kind::Triple => {
437                            state.selection.select_line(line, &state.paragraph);
438                            state.dragging = Some(Dragging::Line);
439                        }
440                    }
441
442                    state.last_click = Some(click);
443
444                    shell.capture_event();
445                } else {
446                    state.selection = Selection::default();
447                }
448            }
449            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
450            | Event::Touch(touch::Event::FingerLifted { .. })
451            | Event::Touch(touch::Event::FingerLost { .. }) => {
452                state.dragging = None;
453            }
454            Event::Mouse(mouse::Event::CursorMoved { .. })
455            | Event::Touch(touch::Event::FingerMoved { .. }) => {
456                if let Some(position) = click_position
457                    && let Some(dragging) = state.dragging
458                {
459                    let (line, index) = state
460                        .grapheme_line_and_index(position)
461                        .unwrap_or((0, 0));
462
463                    match dragging {
464                        Dragging::Grapheme => {
465                            let new_end = SelectionEnd { line, index };
466
467                            state.selection.change_selection(new_end);
468                        }
469                        Dragging::Word => {
470                            let new_end = SelectionEnd { line, index };
471
472                            state.selection.change_selection_by_word(
473                                new_end,
474                                &state.paragraph,
475                            );
476                        }
477                        Dragging::Line => {
478                            state.selection.change_selection_by_line(
479                                line,
480                                &state.paragraph,
481                            );
482                        }
483                    };
484                }
485            }
486            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
487                match key.as_ref() {
488                    keyboard::Key::Character("c")
489                        if state.keyboard_modifiers.command()
490                            && !state.selection.is_empty() =>
491                    {
492                        clipboard.write(
493                            clipboard::Kind::Standard,
494                            state.selection.text(&state.paragraph),
495                        );
496
497                        shell.capture_event();
498                    }
499                    keyboard::Key::Character("a")
500                        if state.keyboard_modifiers.command()
501                            && !state.selection.is_empty() =>
502                    {
503                        state.selection.select_all(&state.paragraph);
504
505                        shell.capture_event();
506                    }
507                    keyboard::Key::Named(key::Named::Home)
508                        if state.keyboard_modifiers.shift()
509                            && !state.selection.is_empty() =>
510                    {
511                        if state.keyboard_modifiers.jump() {
512                            state.selection.select_beginning();
513                        } else {
514                            state.selection.select_line_beginning();
515                        }
516
517                        shell.capture_event();
518                    }
519                    keyboard::Key::Named(key::Named::End)
520                        if state.keyboard_modifiers.shift()
521                            && !state.selection.is_empty() =>
522                    {
523                        if state.keyboard_modifiers.jump() {
524                            state.selection.select_end(&state.paragraph);
525                        } else {
526                            state.selection.select_line_end(&state.paragraph);
527                        }
528
529                        shell.capture_event();
530                    }
531                    keyboard::Key::Named(key::Named::ArrowLeft)
532                        if state.keyboard_modifiers.shift()
533                            && !state.selection.is_empty() =>
534                    {
535                        if state.keyboard_modifiers.macos_command() {
536                            state.selection.select_line_beginning();
537                        } else if state.keyboard_modifiers.jump() {
538                            state
539                                .selection
540                                .select_left_by_words(&state.paragraph);
541                        } else {
542                            state.selection.select_left(&state.paragraph);
543                        }
544
545                        shell.capture_event();
546                    }
547                    keyboard::Key::Named(key::Named::ArrowRight)
548                        if state.keyboard_modifiers.shift()
549                            && !state.selection.is_empty() =>
550                    {
551                        if state.keyboard_modifiers.macos_command() {
552                            state.selection.select_line_end(&state.paragraph);
553                        } else if state.keyboard_modifiers.jump() {
554                            state
555                                .selection
556                                .select_right_by_words(&state.paragraph);
557                        } else {
558                            state.selection.select_right(&state.paragraph);
559                        }
560
561                        shell.capture_event();
562                    }
563                    keyboard::Key::Named(key::Named::ArrowUp)
564                        if state.keyboard_modifiers.shift()
565                            && !state.selection.is_empty() =>
566                    {
567                        if state.keyboard_modifiers.macos_command() {
568                            state.selection.select_beginning();
569                        } else if state.keyboard_modifiers.jump() {
570                            state.selection.select_line_beginning();
571                        } else {
572                            state.selection.select_up(&state.paragraph);
573                        }
574
575                        shell.capture_event();
576                    }
577                    keyboard::Key::Named(key::Named::ArrowDown)
578                        if state.keyboard_modifiers.shift()
579                            && !state.selection.is_empty() =>
580                    {
581                        if state.keyboard_modifiers.macos_command() {
582                            state.selection.select_end(&state.paragraph);
583                        } else if state.keyboard_modifiers.jump() {
584                            state.selection.select_line_end(&state.paragraph);
585                        } else {
586                            state.selection.select_down(&state.paragraph);
587                        }
588
589                        shell.capture_event();
590                    }
591                    keyboard::Key::Named(key::Named::Escape) => {
592                        state.dragging = None;
593                        state.selection = Selection::default();
594
595                        state.keyboard_modifiers =
596                            keyboard::Modifiers::default();
597
598                        if state.selection != selection_before {
599                            shell.capture_event();
600                        }
601                    }
602                    _ => {}
603                }
604            }
605            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
606                state.keyboard_modifiers = *modifiers;
607            }
608            _ => {}
609        }
610
611        if state.is_hovered != was_hovered
612            || state.selection != selection_before
613        {
614            shell.request_redraw();
615        }
616    }
617
618    fn draw(
619        &self,
620        tree: &Tree,
621        renderer: &mut Renderer,
622        theme: &Theme,
623        defaults: &renderer::Style,
624        layout: Layout<'_>,
625        _cursor_position: mouse::Cursor,
626        viewport: &core::Rectangle,
627    ) {
628        if !layout.bounds().intersects(viewport) {
629            return;
630        }
631
632        let state = tree.state.downcast_ref::<State>();
633        let style = theme.style(&self.class);
634
635        if !state.selection.is_empty() {
636            let bounds = layout.bounds();
637
638            let (rows, mut start, mut end) = state.selection_end_points();
639            start += core::Vector::new(bounds.x, bounds.y);
640            end += core::Vector::new(bounds.x, bounds.y);
641
642            let line_height = self
643                .format
644                .line_height
645                .to_absolute(
646                    self.format.size.unwrap_or_else(|| renderer.default_size()),
647                )
648                .0;
649
650            let baseline_y = bounds.y
651                + (((start.y - bounds.y) * 10.0).ceil() / 10.0 / line_height)
652                    .floor()
653                    * line_height;
654
655            for row in 0..rows {
656                let (x, width) = if row == 0 {
657                    (
658                        start.x,
659                        if rows == 1 {
660                            end.x.min(bounds.x + bounds.width) - start.x
661                        } else {
662                            bounds.x + bounds.width - start.x
663                        },
664                    )
665                } else if row == rows - 1 {
666                    (bounds.x, end.x - bounds.x)
667                } else {
668                    (bounds.x, bounds.width)
669                };
670                let y = baseline_y + row as f32 * line_height;
671
672                renderer.fill_quad(
673                    renderer::Quad {
674                        bounds: core::Rectangle {
675                            x,
676                            y,
677                            width,
678                            height: line_height,
679                        },
680                        snap: true,
681                        ..Default::default()
682                    },
683                    style.selection,
684                );
685            }
686        }
687
688        draw(
689            renderer,
690            defaults,
691            layout.bounds(),
692            &state.paragraph,
693            style,
694            viewport,
695        );
696    }
697
698    fn operate(
699        &mut self,
700        _state: &mut Tree,
701        layout: Layout<'_>,
702        _renderer: &Renderer,
703        operation: &mut dyn Operation,
704    ) {
705        operation.text(None, layout.bounds(), &self.fragment);
706    }
707
708    fn mouse_interaction(
709        &self,
710        tree: &Tree,
711        _layout: Layout<'_>,
712        _cursor: mouse::Cursor,
713        _viewport: &core::Rectangle,
714        _renderer: &Renderer,
715    ) -> mouse::Interaction {
716        let state = tree.state.downcast_ref::<State>();
717
718        if state.is_hovered {
719            mouse::Interaction::Text
720        } else {
721            mouse::Interaction::default()
722        }
723    }
724}
725
726/// Produces the [`layout::Node`] of a [`Text`] widget.
727///
728/// [`layout::Node`]: https://docs.iced.rs/iced_core/layout/struct.Node.html
729pub fn layout<Renderer>(
730    state: &mut State,
731    renderer: &Renderer,
732    limits: &layout::Limits,
733    content: &str,
734    format: Format<Font>,
735) -> layout::Node
736where
737    Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
738{
739    layout::sized(limits, format.width, format.height, |limits| {
740        let bounds = limits.max();
741
742        let size = format.size.unwrap_or_else(|| renderer.default_size());
743        let font = format.font.unwrap_or_else(|| renderer.default_font());
744
745        state.update(text::Text {
746            content,
747            bounds,
748            size,
749            line_height: format.line_height,
750            font,
751            align_x: format.align_x,
752            align_y: format.align_y,
753            shaping: format.shaping,
754            wrapping: format.wrapping,
755        });
756
757        state.paragraph.min_bounds()
758    })
759}
760
761/// Draws text using the same logic as the [`Text`] widget.
762pub fn draw<Renderer>(
763    renderer: &mut Renderer,
764    style: &renderer::Style,
765    bounds: core::Rectangle,
766    paragraph: &Paragraph,
767    appearance: Style,
768    viewport: &core::Rectangle,
769) where
770    Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
771{
772    let anchor = bounds.anchor(
773        paragraph.min_bounds(),
774        paragraph.align_x(),
775        paragraph.align_y(),
776    );
777
778    renderer.fill_paragraph(
779        paragraph,
780        anchor,
781        appearance.color.unwrap_or(style.text_color),
782        *viewport,
783    );
784}
785
786impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>>
787    for Element<'a, Message, Theme, Renderer>
788where
789    Theme: Catalog + 'a,
790    Renderer: text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
791{
792    fn from(
793        text: Text<'a, Theme, Renderer>,
794    ) -> Element<'a, Message, Theme, Renderer> {
795        Element::new(text)
796    }
797}
798
799impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
800where
801    Theme: Catalog + 'a,
802    Renderer: text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
803{
804    fn from(content: &'a str) -> Self {
805        Self::new(content)
806    }
807}
808
809/// The appearance of some text.
810#[derive(Debug, Clone, Copy, PartialEq, Default)]
811pub struct Style {
812    /// The [`Color`] of the text.
813    ///
814    /// The default, `None`, means using the inherited color.
815    pub color: Option<Color>,
816    /// The [`Color`] of text selections.
817    pub selection: Color,
818}
819
820/// The theme catalog of a [`Text`].
821pub trait Catalog: Sized {
822    /// The item class of this [`Catalog`].
823    type Class<'a>;
824
825    /// The default class produced by this [`Catalog`].
826    fn default<'a>() -> Self::Class<'a>;
827
828    /// The [`Style`] of a class with the given status.
829    fn style(&self, item: &Self::Class<'_>) -> Style;
830}
831
832/// A styling function for a [`Text`].
833///
834/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
835pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
836
837impl Catalog for Theme {
838    type Class<'a> = StyleFn<'a, Self>;
839
840    fn default<'a>() -> Self::Class<'a> {
841        Box::new(default)
842    }
843
844    fn style(&self, class: &Self::Class<'_>) -> Style {
845        class(self)
846    }
847}
848
849/// The default text styling; color is inherited.
850pub fn default(theme: &Theme) -> Style {
851    Style {
852        color: None,
853        selection: theme.extended_palette().primary.weak.color,
854    }
855}