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