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