Skip to main content

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, cosmic_text};
22pub use rich::Rich;
23use text::{Ellipsis, LineHeight, Shaping, Wrapping};
24pub use text::{Fragment, Highlighter, IntoFragment, Span};
25
26use crate::click;
27use crate::core::alignment;
28use crate::core::keyboard::{self, key};
29use crate::core::layout;
30use crate::core::mouse;
31use crate::core::renderer;
32use crate::core::text;
33use crate::core::text::Paragraph as _;
34use crate::core::time::Duration;
35use crate::core::touch;
36use crate::core::widget;
37use crate::core::widget::text::Format;
38use crate::core::widget::tree::{self, Tree};
39use crate::core::{
40    self, Color, Event, Font, Layout, Length, Pixels, Point, Size, Theme,
41    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) -> impl Widget<'_, Message> {
56///     text("Hello, this is iced!")
57///         .size(20)
58///         .into()
59/// }
60/// ```
61#[must_use]
62pub struct Text<'a, Theme = iced_widget::Theme>
63where
64    Theme: Catalog,
65{
66    fragment: Fragment<'a>,
67    format: Format,
68    click_interval: Option<Duration>,
69    class: Theme::Class<'a>,
70}
71
72impl<'a, Theme> Text<'a, Theme>
73where
74    Theme: Catalog,
75{
76    /// Create a new fragment of [`Text`] with the given contents.
77    pub fn new(fragment: impl IntoFragment<'a>) -> Self {
78        Self {
79            fragment: fragment.into_fragment(),
80            format: Format::default(),
81            click_interval: None,
82            class: Theme::default(),
83        }
84    }
85
86    /// Sets the size of the [`Text`].
87    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
88        self.format.size = Some(size.into());
89        self
90    }
91
92    /// Sets the [`LineHeight`] of the [`Text`].
93    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
94        self.format.line_height = Some(line_height.into());
95        self
96    }
97
98    /// Sets the [`Font`] of the [`Text`].
99    pub fn font(mut self, font: impl Into<Font>) -> Self {
100        self.format.font = Some(font.into());
101        self
102    }
103
104    /// Sets the [`Font`] of the [`Text`], if `Some`.
105    pub fn font_maybe(mut self, font: impl Into<Option<Font>>) -> Self {
106        self.format.font = font.into();
107        self
108    }
109
110    /// Sets the width of the [`Text`] boundaries.
111    pub fn width(mut self, width: impl Into<Length>) -> Self {
112        self.format.width = width.into();
113        self
114    }
115
116    /// Sets the height of the [`Text`] boundaries.
117    pub fn height(mut self, height: impl Into<Length>) -> Self {
118        self.format.height = height.into();
119        self
120    }
121
122    /// Centers the [`Text`], both horizontally and vertically.
123    pub fn center(self) -> Self {
124        self.align_x(alignment::Horizontal::Center)
125            .align_y(alignment::Vertical::Center)
126    }
127
128    /// Sets the [`alignment::Horizontal`] of the [`Text`].
129    pub fn align_x(mut self, alignment: impl Into<text::Alignment>) -> Self {
130        self.format.align_x = alignment.into();
131        self
132    }
133
134    /// Sets the [`alignment::Vertical`] of the [`Text`].
135    pub fn align_y(
136        mut self,
137        alignment: impl Into<alignment::Vertical>,
138    ) -> Self {
139        self.format.align_y = alignment.into();
140        self
141    }
142
143    /// Sets the [`Shaping`] strategy of the [`Text`].
144    pub fn shaping(mut self, shaping: Shaping) -> Self {
145        self.format.shaping = shaping;
146        self
147    }
148
149    /// Sets the [`Wrapping`] strategy of the [`Text`].
150    pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
151        self.format.wrapping = wrapping;
152        self
153    }
154
155    /// Sets the [`Ellipsis`] strategy of the [`Text`].
156    pub fn ellipsis(mut self, ellipsis: Ellipsis) -> Self {
157        self.format.ellipsis = ellipsis;
158        self
159    }
160
161    /// The maximum delay required for two consecutive clicks to be interpreted as a double click
162    /// (also applies to triple clicks).
163    ///
164    /// Defaults to 300ms.
165    pub fn click_interval(mut self, click_interval: Duration) -> Self {
166        self.click_interval = Some(click_interval);
167        self
168    }
169
170    /// Sets the style of the [`Text`].
171    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
172    where
173        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
174    {
175        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
176        self
177    }
178
179    /// Sets the style class of the [`Text`].
180    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
181        self.class = class.into();
182        self
183    }
184}
185
186/// The internal state of a [`Text`] widget.
187#[derive(Debug, Default, Clone)]
188pub struct State {
189    paragraph: Paragraph,
190    content: String,
191    is_hovered: bool,
192    selection: Selection,
193    dragging: Option<Dragging>,
194    last_click: Option<click::Click>,
195    keyboard_modifiers: keyboard::Modifiers,
196    visual_lines_bounds: Vec<core::Rectangle>,
197}
198
199/// The type of dragging selection.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201#[allow(missing_docs)]
202pub enum Dragging {
203    Grapheme,
204    Word,
205    Line,
206}
207
208impl State {
209    fn grapheme_line_and_index(
210        &self,
211        point: Point,
212        bounds: core::Rectangle,
213    ) -> Option<(usize, usize)> {
214        let bounded_x = if point.y < bounds.y {
215            bounds.x
216        } else if point.y > bounds.y + bounds.height {
217            bounds.x + bounds.width
218        } else {
219            point.x.max(bounds.x).min(bounds.x + bounds.width)
220        };
221        let bounded_y = point.y.max(bounds.y).min(bounds.y + bounds.height);
222        let bounded_point = Point::new(bounded_x, bounded_y);
223        let mut relative_point =
224            bounded_point - core::Vector::new(bounds.x, bounds.y);
225
226        let buffer = self.paragraph.buffer();
227        let line_height = buffer.metrics().line_height;
228        let visual_line = (relative_point.y / line_height).floor() as usize;
229        let visual_line_start_offset = self
230            .visual_lines_bounds
231            .get(visual_line)
232            .map(|r| r.x)
233            .unwrap_or_default();
234        let visual_line_end = self
235            .visual_lines_bounds
236            .get(visual_line)
237            .map(|r| r.x + r.width)
238            .unwrap_or_default();
239
240        if relative_point.x < visual_line_start_offset {
241            relative_point.x = visual_line_start_offset;
242        }
243
244        if relative_point.x > visual_line_end {
245            relative_point.x = visual_line_end;
246        }
247
248        let cursor = buffer.hit(relative_point.x, relative_point.y)?;
249        let value = buffer.lines[cursor.line].text();
250
251        Some((
252            cursor.line,
253            unicode_segmentation::UnicodeSegmentation::graphemes(
254                &value[..cursor.index.min(value.len())],
255                true,
256            )
257            .count(),
258        ))
259    }
260
261    fn selection(&self) -> Vec<core::Rectangle> {
262        let Selection { start, end, .. } = self.selection;
263
264        let buffer = self.paragraph.buffer();
265        let line_height = self.paragraph.buffer().metrics().line_height;
266        let selected_lines = end.line - start.line + 1;
267
268        let visual_lines_offset = visual_lines_offset(start.line, buffer);
269
270        buffer
271            .lines
272            .iter()
273            .skip(start.line)
274            .take(selected_lines)
275            .enumerate()
276            .flat_map(|(i, line)| {
277                highlight_line(
278                    line,
279                    if i == 0 { start.index } else { 0 },
280                    if i == selected_lines - 1 {
281                        end.index
282                    } else {
283                        line.text().len()
284                    },
285                )
286            })
287            .enumerate()
288            .filter_map(|(visual_line, (x, width))| {
289                if width > 0.0 {
290                    Some(core::Rectangle {
291                        x,
292                        width,
293                        y: (visual_line as i32 + visual_lines_offset) as f32
294                            * line_height
295                            - buffer.scroll().vertical,
296                        height: line_height,
297                    })
298                } else {
299                    None
300                }
301            })
302            .collect()
303    }
304
305    fn update(&mut self, text: text::Text<&str>) {
306        if self.content != text.content {
307            text.content.clone_into(&mut self.content);
308            self.paragraph = Paragraph::with_text(text);
309            self.update_visual_bounds();
310            return;
311        }
312
313        match self.paragraph.compare(text.with_content(())) {
314            text::Difference::None => {}
315            text::Difference::Bounds => {
316                self.paragraph.resize(text.bounds);
317                self.update_visual_bounds();
318            }
319            text::Difference::Shape => {
320                self.paragraph = Paragraph::with_text(text);
321                self.update_visual_bounds();
322            }
323        }
324    }
325
326    fn update_visual_bounds(&mut self) {
327        let buffer = self.paragraph.buffer();
328        let line_height = buffer.metrics().line_height;
329        self.visual_lines_bounds = buffer
330            .lines
331            .iter()
332            .flat_map(|line| highlight_line(line, 0, line.text().len()))
333            .enumerate()
334            .map(|(visual_line, (x, width))| core::Rectangle {
335                x,
336                width,
337                y: visual_line as f32 * line_height - buffer.scroll().vertical,
338                height: line_height,
339            })
340            .collect();
341    }
342}
343
344impl widget::operation::Text for State {
345    fn text(&self) -> Fragment<'_> {
346        self.paragraph
347            .buffer()
348            .lines
349            .iter()
350            .fold(String::new(), |mut text, next| {
351                text.push_str(next.text());
352                text
353            })
354            .into()
355    }
356
357    fn select(&mut self, start: Point, end: Point, target: text::Target) {
358        let bounds = core::Rectangle::INFINITE;
359
360        let start = {
361            let (line, index) = self
362                .grapheme_line_and_index(start, bounds)
363                .unwrap_or((0, 0));
364
365            SelectionEnd { line, index }
366        };
367
368        let end = {
369            let (line, index) =
370                self.grapheme_line_and_index(end, bounds).unwrap_or((0, 0));
371
372            SelectionEnd { line, index }
373        };
374
375        match target {
376            text::Target::Character => self.selection.select_range(start, end),
377            text::Target::Word => {
378                self.selection.select_word(
379                    start.line,
380                    start.index,
381                    &self.paragraph,
382                );
383
384                self.selection
385                    .change_selection_by_word(end, &self.paragraph);
386            }
387            text::Target::Line => {
388                self.selection.select_line(start.line, &self.paragraph);
389
390                self.selection
391                    .change_selection_by_line(end.line, &self.paragraph);
392            }
393        };
394    }
395
396    fn select_all(&mut self) {
397        self.selection.select_all(&self.paragraph);
398    }
399
400    fn deselect(&mut self) {
401        self.selection.clear();
402    }
403
404    fn copy(&mut self) -> Option<String> {
405        (!self.selection.is_empty())
406            .then(|| self.selection.text(&self.paragraph))
407    }
408}
409
410impl<Theme> widget::Meta for Text<'_, Theme> where Theme: Catalog {}
411
412impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
413    for Text<'_, Theme>
414where
415    Theme: Catalog,
416    Renderer: text::Renderer<Paragraph = Paragraph>,
417{
418    fn tag(&self) -> tree::Tag {
419        tree::Tag::of::<State>()
420    }
421
422    fn state(&self) -> tree::State {
423        tree::State::new(State::default())
424    }
425
426    fn size(&self) -> Size<Length> {
427        Size {
428            width: self.format.width,
429            height: self.format.height,
430        }
431    }
432
433    fn layout(
434        &mut self,
435        tree: &mut Tree,
436        renderer: &Renderer,
437        limits: &layout::Limits,
438    ) {
439        tree.size = layout(
440            tree.state.downcast_mut::<State>(),
441            renderer,
442            limits,
443            &self.fragment,
444            self.format,
445        );
446    }
447
448    fn update(
449        &mut self,
450        tree: &mut Tree,
451        event: &Event,
452        layout: Layout,
453        cursor: mouse::Cursor,
454        _renderer: &Renderer,
455        shell: &mut core::Shell<'_, Message>,
456        viewport: &core::Rectangle,
457    ) {
458        let state = tree.state.downcast_mut::<State>();
459
460        let bounds = layout.bounds();
461        let click_position = cursor.position_in(bounds);
462
463        if viewport.intersection(&bounds).is_none()
464            && state.selection.is_empty()
465            && state.dragging.is_none()
466        {
467            return;
468        }
469
470        let was_hovered = state.is_hovered;
471        let selection_before = state.selection;
472        state.is_hovered = click_position.is_some();
473
474        match event {
475            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
476            | Event::Touch(touch::Event::FingerPressed { .. }) => {
477                if let Some(position) = cursor.position_over(bounds) {
478                    let click = click::Click::new(
479                        position,
480                        mouse::Button::Left,
481                        state.last_click,
482                        self.click_interval,
483                    );
484
485                    let (line, index) = state
486                        .grapheme_line_and_index(position, bounds)
487                        .unwrap_or((0, 0));
488
489                    match click.kind() {
490                        click::Kind::Single => {
491                            let new_end = SelectionEnd { line, index };
492
493                            if state.keyboard_modifiers.shift() {
494                                state.selection.change_selection(new_end);
495                            } else {
496                                state.selection.select_range(new_end, new_end);
497                            }
498
499                            state.dragging = Some(Dragging::Grapheme);
500                        }
501                        click::Kind::Double => {
502                            state.selection.select_word(
503                                line,
504                                index,
505                                &state.paragraph,
506                            );
507
508                            state.dragging = Some(Dragging::Word);
509                        }
510                        click::Kind::Triple => {
511                            state.selection.select_line(line, &state.paragraph);
512                            state.dragging = Some(Dragging::Line);
513                        }
514                    }
515
516                    state.last_click = Some(click);
517
518                    shell.capture_event();
519                } else {
520                    state.selection.clear();
521                }
522            }
523            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
524            | Event::Touch(touch::Event::FingerLifted { .. })
525            | Event::Touch(touch::Event::FingerLost { .. }) => {
526                state.dragging = None;
527            }
528            Event::Mouse(mouse::Event::CursorMoved { .. })
529            | Event::Touch(touch::Event::FingerMoved { .. }) => {
530                if let Some(position) = cursor.land().position()
531                    && let Some(dragging) = state.dragging
532                {
533                    let (line, index) = state
534                        .grapheme_line_and_index(position, bounds)
535                        .unwrap_or((0, 0));
536
537                    match dragging {
538                        Dragging::Grapheme => {
539                            let new_end = SelectionEnd { line, index };
540
541                            state.selection.change_selection(new_end);
542                        }
543                        Dragging::Word => {
544                            let new_end = SelectionEnd { line, index };
545
546                            state.selection.change_selection_by_word(
547                                new_end,
548                                &state.paragraph,
549                            );
550                        }
551                        Dragging::Line => {
552                            state.selection.change_selection_by_line(
553                                line,
554                                &state.paragraph,
555                            );
556                        }
557                    };
558                }
559            }
560            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
561                match key.as_ref() {
562                    keyboard::Key::Character("c")
563                        if state.keyboard_modifiers.command()
564                            && !state.selection.is_empty() =>
565                    {
566                        shell.write_clipboard(
567                            state.selection.text(&state.paragraph),
568                        );
569
570                        shell.capture_event();
571                    }
572                    keyboard::Key::Character("a")
573                        if state.keyboard_modifiers.command()
574                            && !state.selection.is_empty() =>
575                    {
576                        state.selection.select_all(&state.paragraph);
577
578                        shell.capture_event();
579                    }
580                    keyboard::Key::Named(key::Named::Home)
581                        if state.keyboard_modifiers.shift()
582                            && !state.selection.is_empty() =>
583                    {
584                        if state.keyboard_modifiers.jump() {
585                            state.selection.select_beginning();
586                        } else {
587                            state.selection.select_line_beginning();
588                        }
589
590                        shell.capture_event();
591                    }
592                    keyboard::Key::Named(key::Named::End)
593                        if state.keyboard_modifiers.shift()
594                            && !state.selection.is_empty() =>
595                    {
596                        if state.keyboard_modifiers.jump() {
597                            state.selection.select_end(&state.paragraph);
598                        } else {
599                            state.selection.select_line_end(&state.paragraph);
600                        }
601
602                        shell.capture_event();
603                    }
604                    keyboard::Key::Named(key::Named::ArrowLeft)
605                        if state.keyboard_modifiers.shift()
606                            && !state.selection.is_empty() =>
607                    {
608                        if state.keyboard_modifiers.macos_command() {
609                            state.selection.select_line_beginning();
610                        } else if state.keyboard_modifiers.jump() {
611                            state
612                                .selection
613                                .select_left_by_words(&state.paragraph);
614                        } else {
615                            state.selection.select_left(&state.paragraph);
616                        }
617
618                        shell.capture_event();
619                    }
620                    keyboard::Key::Named(key::Named::ArrowRight)
621                        if state.keyboard_modifiers.shift()
622                            && !state.selection.is_empty() =>
623                    {
624                        if state.keyboard_modifiers.macos_command() {
625                            state.selection.select_line_end(&state.paragraph);
626                        } else if state.keyboard_modifiers.jump() {
627                            state
628                                .selection
629                                .select_right_by_words(&state.paragraph);
630                        } else {
631                            state.selection.select_right(&state.paragraph);
632                        }
633
634                        shell.capture_event();
635                    }
636                    keyboard::Key::Named(key::Named::ArrowUp)
637                        if state.keyboard_modifiers.shift()
638                            && !state.selection.is_empty() =>
639                    {
640                        if state.keyboard_modifiers.macos_command() {
641                            state.selection.select_beginning();
642                        } else if state.keyboard_modifiers.jump() {
643                            state.selection.select_line_beginning();
644                        } else {
645                            state.selection.select_up(&state.paragraph);
646                        }
647
648                        shell.capture_event();
649                    }
650                    keyboard::Key::Named(key::Named::ArrowDown)
651                        if state.keyboard_modifiers.shift()
652                            && !state.selection.is_empty() =>
653                    {
654                        if state.keyboard_modifiers.macos_command() {
655                            state.selection.select_end(&state.paragraph);
656                        } else if state.keyboard_modifiers.jump() {
657                            state.selection.select_line_end(&state.paragraph);
658                        } else {
659                            state.selection.select_down(&state.paragraph);
660                        }
661
662                        shell.capture_event();
663                    }
664                    keyboard::Key::Named(key::Named::Escape) => {
665                        state.dragging = None;
666                        state.selection.clear();
667
668                        state.keyboard_modifiers =
669                            keyboard::Modifiers::default();
670
671                        if state.selection != selection_before {
672                            shell.capture_event();
673                        }
674                    }
675                    _ => {}
676                }
677            }
678            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
679                state.keyboard_modifiers = *modifiers;
680            }
681            _ => {}
682        }
683
684        if state.is_hovered != was_hovered
685            || state.selection != selection_before
686        {
687            shell.request_redraw();
688        }
689    }
690
691    fn draw(
692        &self,
693        tree: &Tree,
694        renderer: &mut Renderer,
695        theme: &Theme,
696        defaults: &renderer::Style,
697        layout: Layout,
698        _cursor_position: mouse::Cursor,
699        viewport: &core::Rectangle,
700    ) {
701        if !layout.bounds().intersects(viewport) {
702            return;
703        }
704
705        let state = tree.state.downcast_ref::<State>();
706        let style = theme.style(&self.class);
707
708        if !state.selection.is_empty() {
709            let bounds = layout.bounds();
710            let translation = bounds.position() - Point::ORIGIN;
711            let ranges = state.selection();
712
713            for range in ranges
714                .into_iter()
715                .filter_map(|range| bounds.intersection(&(range + translation)))
716            {
717                renderer.fill_quad(
718                    renderer::Quad {
719                        bounds: range,
720                        ..renderer::Quad::default()
721                    },
722                    style.selection,
723                );
724            }
725        }
726
727        draw(
728            renderer,
729            defaults,
730            layout.bounds(),
731            &state.paragraph,
732            style,
733            viewport,
734        );
735    }
736
737    fn operate(
738        &mut self,
739        tree: &mut Tree,
740        layout: Layout,
741        _viewport: &core::Rectangle,
742        _renderer: &Renderer,
743        operation: &mut dyn widget::Operation,
744    ) {
745        let state = tree.state.downcast_mut::<State>();
746        operation.text(None, layout.bounds(), state);
747    }
748
749    fn mouse_interaction(
750        &self,
751        tree: &Tree,
752        _layout: Layout,
753        _cursor: mouse::Cursor,
754        _viewport: &core::Rectangle,
755        _renderer: &Renderer,
756    ) -> mouse::Interaction {
757        let state = tree.state.downcast_ref::<State>();
758
759        if state.is_hovered || state.dragging.is_some() {
760            mouse::Interaction::Text
761        } else {
762            mouse::Interaction::default()
763        }
764    }
765}
766
767/// Produces the [`layout::Node`] of a [`Text`] widget.
768///
769/// [`layout::Node`]: https://docs.iced.rs/iced_core/layout/struct.Node.html
770pub fn layout<Renderer>(
771    state: &mut State,
772    renderer: &Renderer,
773    limits: &layout::Limits,
774    content: &str,
775    format: Format,
776) -> Size
777where
778    Renderer: text::Renderer<Paragraph = Paragraph>,
779{
780    layout::sized(limits, format.width, format.height, |limits| {
781        let bounds = limits.bounds();
782
783        let size = format.size.unwrap_or_else(|| renderer.text_size());
784        let font = format.font.unwrap_or_else(|| renderer.font());
785        let line_height =
786            format.line_height.unwrap_or_else(|| renderer.line_height());
787
788        state.update(text::Text {
789            content,
790            bounds,
791            size,
792            line_height,
793            font,
794            align_x: format.align_x,
795            align_y: format.align_y,
796            shaping: format.shaping,
797            wrapping: format.wrapping,
798            ellipsis: format.ellipsis,
799            hint_factor: renderer.hint_factor(),
800        });
801
802        state.paragraph.min_bounds()
803    })
804}
805
806/// Draws text using the same logic as the [`Text`] widget.
807pub fn draw<Renderer>(
808    renderer: &mut Renderer,
809    style: &renderer::Style,
810    bounds: core::Rectangle,
811    paragraph: &Paragraph,
812    appearance: Style,
813    viewport: &core::Rectangle,
814) where
815    Renderer: text::Renderer<Paragraph = Paragraph>,
816{
817    let anchor = bounds.anchor(
818        paragraph.min_bounds(),
819        paragraph.align_x(),
820        paragraph.align_y(),
821    );
822
823    renderer.fill_paragraph(
824        paragraph,
825        anchor,
826        appearance.color.unwrap_or(style.text_color),
827        *viewport,
828    );
829}
830
831impl<'a, Theme> From<&'a str> for Text<'a, Theme>
832where
833    Theme: Catalog + 'a,
834{
835    fn from(content: &'a str) -> Self {
836        Self::new(content)
837    }
838}
839
840/// The appearance of some text.
841#[derive(Debug, Clone, Copy, PartialEq, Default)]
842pub struct Style {
843    /// The [`Color`] of the text.
844    ///
845    /// The default, `None`, means using the inherited color.
846    pub color: Option<Color>,
847    /// The [`Color`] of text selections.
848    pub selection: Color,
849}
850
851/// The theme catalog of a [`Text`].
852pub trait Catalog: Sized {
853    /// The item class of this [`Catalog`].
854    type Class<'a>;
855
856    /// The default class produced by this [`Catalog`].
857    fn default<'a>() -> Self::Class<'a>;
858
859    /// The [`Style`] of a class with the given status.
860    fn style(&self, item: &Self::Class<'_>) -> Style;
861}
862
863/// A styling function for a [`Text`].
864///
865/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
866pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
867
868impl Catalog for Theme {
869    type Class<'a> = StyleFn<'a, Self>;
870
871    fn default<'a>() -> Self::Class<'a> {
872        Box::new(default)
873    }
874
875    fn style(&self, class: &Self::Class<'_>) -> Style {
876        class(self)
877    }
878}
879
880/// The default text styling; color is inherited.
881pub fn default(theme: &Theme) -> Style {
882    Style {
883        color: None,
884        selection: theme.palette().primary.weak.color,
885    }
886}
887
888fn highlight_line(
889    line: &cosmic_text::BufferLine,
890    from: usize,
891    to: usize,
892) -> impl Iterator<Item = (f32, f32)> + '_ {
893    let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default();
894
895    // Check for multi codepoint glyphs for each previous visual line
896    let mut previous_diff = 0;
897    let previous_lines_diff = line
898        .layout_opt()
899        .map(Vec::as_slice)
900        .unwrap_or_default()
901        .iter()
902        .enumerate()
903        .map(move |(line_nr, visual_line)| {
904            if line_nr == 0 {
905                let current_diff = previous_diff
906                    + visual_line.glyphs.iter().fold(0, |d, g| {
907                        (d + g.start.abs_diff(g.end)).saturating_sub(1)
908                    });
909                previous_diff = current_diff;
910                0
911            } else {
912                let current_diff = previous_diff
913                    + visual_line
914                        .glyphs
915                        .iter()
916                        .fold(0, |d, g| d + g.start.abs_diff(g.end) - 1);
917                let previous_diff_temp = previous_diff;
918                previous_diff = current_diff;
919                previous_diff_temp
920            }
921        });
922
923    layout.iter().zip(previous_lines_diff).map(
924        move |(visual_line, previous_lines_diff)| {
925            let start = visual_line
926                .glyphs
927                .first()
928                .map(|glyph| glyph.start)
929                .unwrap_or(0);
930            let end = visual_line
931                .glyphs
932                .last()
933                .map(|glyph| glyph.end)
934                .unwrap_or(0);
935
936            let to = to + previous_lines_diff;
937            let mut range = start.max(from)..end.min(to);
938
939            let x_offset = visual_line
940                .glyphs
941                .first()
942                .map(|glyph| glyph.x)
943                .unwrap_or_default();
944
945            if range.is_empty() {
946                (x_offset, 0.0)
947            } else if range.start == start && range.end == end {
948                (x_offset, visual_line.w)
949            } else {
950                let mut x = 0.0;
951                let mut width = 0.0;
952                for glyph in &visual_line.glyphs {
953                    let glyph_count = glyph.start.abs_diff(glyph.end);
954
955                    // Check for multi codepoint glyphs before or within the range
956                    if glyph_count > 1 {
957                        if range.start > glyph.start {
958                            range.start += glyph_count - 1;
959                            range.end += glyph_count - 1;
960                        } else if range.end > glyph.start {
961                            range.end += glyph_count - 1;
962                        }
963                    }
964
965                    if range.start > glyph.start {
966                        x += glyph.w;
967                    }
968
969                    if range.start <= glyph.start && range.end > glyph.start {
970                        width += glyph.w;
971                    } else if range.end <= glyph.start {
972                        break;
973                    }
974                }
975
976                (x_offset + x, width)
977            }
978        },
979    )
980}
981
982fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
983    let scroll = buffer.scroll();
984
985    let start = scroll.line.min(line);
986    let end = scroll.line.max(line);
987
988    let visual_lines_offset: usize = buffer.lines[start..]
989        .iter()
990        .take(end - start)
991        .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
992        .sum();
993
994    visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
995}