Skip to main content

iced_selection/text/
rich.rs

1use iced_widget::graphics::text::Paragraph;
2use iced_widget::graphics::text::cosmic_text;
3
4use crate::click;
5use crate::core::alignment;
6use crate::core::border;
7use crate::core::keyboard;
8use crate::core::keyboard::key;
9use crate::core::layout;
10use crate::core::mouse;
11use crate::core::renderer;
12use crate::core::text::{Ellipsis, Fragment, Paragraph as _, Span, Target};
13use crate::core::time::Duration;
14use crate::core::touch;
15use crate::core::widget;
16use crate::core::widget::text::{Alignment, LineHeight, Shaping, Wrapping};
17use crate::core::widget::tree::{self, Tree};
18use crate::core::{
19    self, Border, Event, Font, Layout, Length, Pixels, Point, Rectangle, Shell,
20    Size, Vector, Widget,
21};
22use crate::selection::{Selection, SelectionEnd};
23use crate::text::{Catalog, Dragging, Style, StyleFn};
24
25/// A bunch of [`Rich`] text.
26#[must_use]
27pub struct Rich<'a, Link, Message, Theme = iced_widget::Theme>
28where
29    Link: Clone + 'static,
30    Theme: Catalog,
31{
32    spans: Box<dyn AsRef<[Span<'a, Link>]> + 'a>,
33    size: Option<Pixels>,
34    line_height: Option<LineHeight>,
35    width: Length,
36    height: Length,
37    font: Option<Font>,
38    align_x: Alignment,
39    align_y: alignment::Vertical,
40    wrapping: Wrapping,
41    ellipsis: Ellipsis,
42    click_interval: Option<Duration>,
43    class: Theme::Class<'a>,
44    on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
45    on_link_hover: Option<Box<dyn Fn(Link) -> Message + 'a>>,
46    on_hover_lost: Option<Box<dyn Fn() -> Message + 'a>>,
47}
48
49impl<'a, Link, Message, Theme> Rich<'a, Link, Message, Theme>
50where
51    Link: Clone + 'static,
52    Theme: Catalog,
53{
54    /// Creates a new empty [`Rich`] text.
55    pub fn new() -> Self {
56        Self {
57            spans: Box::new([]),
58            size: None,
59            line_height: None,
60            width: Length::Fit,
61            height: Length::Fit,
62            font: None,
63            align_x: Alignment::Default,
64            align_y: alignment::Vertical::Top,
65            wrapping: Wrapping::default(),
66            ellipsis: Ellipsis::None,
67            click_interval: None,
68            class: Theme::default(),
69            on_link_click: None,
70            on_link_hover: None,
71            on_hover_lost: None,
72        }
73    }
74
75    /// Creates a new [`Rich`] text with the given text spans.
76    pub fn with_spans(spans: impl AsRef<[Span<'a, Link>]> + 'a) -> Self {
77        Self {
78            spans: Box::new(spans),
79            ..Self::new()
80        }
81    }
82
83    /// Sets the default size of the [`Rich`] text.
84    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
85        self.size = Some(size.into());
86        self
87    }
88
89    /// Sets the default [`LineHeight`] of the [`Rich`] text.
90    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
91        self.line_height = Some(line_height.into());
92        self
93    }
94
95    /// Sets the default font of the [`Rich`] text.
96    pub fn font(mut self, font: impl Into<Font>) -> Self {
97        self.font = Some(font.into());
98        self
99    }
100
101    /// Sets the default font of the [`Rich`] text, if `Some`.
102    pub fn font_maybe(mut self, font: impl Into<Option<Font>>) -> Self {
103        self.font = font.into();
104        self
105    }
106
107    /// Sets the width of the [`Rich`] text boundaries.
108    pub fn width(mut self, width: impl Into<Length>) -> Self {
109        self.width = width.into();
110        self
111    }
112
113    /// Sets the height of the [`Rich`] text boundaries.
114    pub fn height(mut self, height: impl Into<Length>) -> Self {
115        self.height = height.into();
116        self
117    }
118
119    /// Centers the [`Rich`] text, both horizontally and vertically.
120    pub fn center(self) -> Self {
121        self.align_x(alignment::Horizontal::Center)
122            .align_y(alignment::Vertical::Center)
123    }
124
125    /// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
126    pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
127        self.align_x = alignment.into();
128        self
129    }
130
131    /// Sets the [`alignment::Vertical`] of the [`Rich`] text.
132    pub fn align_y(
133        mut self,
134        alignment: impl Into<alignment::Vertical>,
135    ) -> Self {
136        self.align_y = alignment.into();
137        self
138    }
139
140    /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
141    pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
142        self.wrapping = wrapping;
143        self
144    }
145
146    /// Sets the [`Ellipsis`] strategy of the [`Rich`] text.
147    pub fn ellipsis(mut self, ellipsis: Ellipsis) -> Self {
148        self.ellipsis = ellipsis;
149        self
150    }
151
152    /// The maximum delay required for two consecutive clicks to be interpreted as a double click
153    /// (also applies to triple clicks).
154    ///
155    /// Defaults to 300ms.
156    pub fn click_interval(mut self, click_interval: Duration) -> Self {
157        self.click_interval = Some(click_interval);
158        self
159    }
160
161    /// Sets the message that will be produced when a link of the [`Rich`] text
162    /// is clicked.
163    ///
164    /// If the spans of the [`Rich`] text contain no links, you may need to call
165    /// this method with `on_link_click(never)` in order for the compiler to infer
166    /// the proper `Link` generic type.
167    pub fn on_link_click(
168        mut self,
169        on_link_click: impl Fn(Link) -> Message + 'a,
170    ) -> Self {
171        self.on_link_click = Some(Box::new(on_link_click));
172        self
173    }
174
175    /// Sets the message that will be produced when a link of the [`Rich`] text
176    /// is hovered.
177    pub fn on_link_hover(
178        mut self,
179        on_link_hovered: impl Fn(Link) -> Message + 'a,
180    ) -> Self {
181        self.on_link_hover = Some(Box::new(on_link_hovered));
182        self
183    }
184
185    /// Sets the message that will be produced when a link of the [`Rich`] text
186    /// is no longer hovered.
187    pub fn on_hover_lost(mut self, on_hover_lost: Message) -> Self
188    where
189        Message: Clone + 'a,
190    {
191        self.on_hover_lost = Some(Box::new(move || on_hover_lost.clone()));
192        self
193    }
194
195    /// Sets the message that will be produced when a link of the [`Rich`] text
196    /// is no longer hovered.
197    pub fn on_hover_lost_with(
198        mut self,
199        on_hover_lost: impl Fn() -> Message + 'a,
200    ) -> Self {
201        self.on_hover_lost = Some(Box::new(on_hover_lost));
202        self
203    }
204
205    /// Sets the default style of the [`Rich`] text.
206    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
207    where
208        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
209    {
210        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
211        self
212    }
213
214    /// Sets the default style class of the [`Rich`] text.
215    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
216        self.class = class.into();
217        self
218    }
219}
220
221impl<'a, Link, Message, Theme> Default for Rich<'a, Link, Message, Theme>
222where
223    Link: Clone + 'a,
224    Theme: Catalog,
225{
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231struct State<Link> {
232    spans: Vec<Span<'static, Link>>,
233    span_pressed: Option<usize>,
234    hovered_link: Option<usize>,
235    paragraph: Paragraph,
236    is_hovered: bool,
237    selection: Selection,
238    dragging: Option<Dragging>,
239    last_click: Option<click::Click>,
240    keyboard_modifiers: keyboard::Modifiers,
241    visual_lines_bounds: Vec<core::Rectangle>,
242}
243
244impl<Link> State<Link> {
245    fn grapheme_line_and_index(
246        &self,
247        point: Point,
248        bounds: core::Rectangle,
249    ) -> Option<(usize, usize)> {
250        let bounded_x = if point.y < bounds.y {
251            bounds.x
252        } else if point.y > bounds.y + bounds.height {
253            bounds.x + bounds.width
254        } else {
255            point.x.max(bounds.x).min(bounds.x + bounds.width)
256        };
257        let bounded_y = point.y.max(bounds.y).min(bounds.y + bounds.height);
258        let bounded_point = Point::new(bounded_x, bounded_y);
259        let mut relative_point =
260            bounded_point - core::Vector::new(bounds.x, bounds.y);
261
262        let buffer = self.paragraph.buffer();
263        let line_height = buffer.metrics().line_height;
264        let visual_line = (relative_point.y / line_height).floor() as usize;
265        let visual_line_start_offset = self
266            .visual_lines_bounds
267            .get(visual_line)
268            .map(|r| r.x)
269            .unwrap_or_default();
270        let visual_line_end = self
271            .visual_lines_bounds
272            .get(visual_line)
273            .map(|r| r.x + r.width)
274            .unwrap_or_default();
275
276        if relative_point.x < visual_line_start_offset {
277            relative_point.x = visual_line_start_offset;
278        }
279
280        if relative_point.x > visual_line_end {
281            relative_point.x = visual_line_end;
282        }
283
284        let cursor = buffer.hit(relative_point.x, relative_point.y)?;
285        let value = buffer.lines[cursor.line].text();
286
287        Some((
288            cursor.line,
289            unicode_segmentation::UnicodeSegmentation::graphemes(
290                &value[..cursor.index.min(value.len())],
291                true,
292            )
293            .count(),
294        ))
295    }
296
297    fn selection(&self) -> Vec<core::Rectangle> {
298        let Selection { start, end, .. } = self.selection;
299
300        let buffer = self.paragraph.buffer();
301        let line_height = self.paragraph.buffer().metrics().line_height;
302        let selected_lines = end.line - start.line + 1;
303
304        let visual_lines_offset = visual_lines_offset(start.line, buffer);
305
306        buffer
307            .lines
308            .iter()
309            .skip(start.line)
310            .take(selected_lines)
311            .enumerate()
312            .flat_map(|(i, line)| {
313                highlight_line(
314                    line,
315                    if i == 0 { start.index } else { 0 },
316                    if i == selected_lines - 1 {
317                        end.index
318                    } else {
319                        line.text().len()
320                    },
321                )
322            })
323            .enumerate()
324            .filter_map(|(visual_line, (x, width))| {
325                if width > 0.0 {
326                    Some(core::Rectangle {
327                        x,
328                        width,
329                        y: (visual_line as i32 + visual_lines_offset) as f32
330                            * line_height
331                            - buffer.scroll().vertical,
332                        height: line_height,
333                    })
334                } else {
335                    None
336                }
337            })
338            .collect()
339    }
340
341    fn update_visual_bounds(&mut self) {
342        let buffer = self.paragraph.buffer();
343        let line_height = buffer.metrics().line_height;
344        self.visual_lines_bounds = buffer
345            .lines
346            .iter()
347            .flat_map(|line| highlight_line(line, 0, line.text().len()))
348            .enumerate()
349            .map(|(visual_line, (x, width))| core::Rectangle {
350                x,
351                width,
352                y: visual_line as f32 * line_height - buffer.scroll().vertical,
353                height: line_height,
354            })
355            .collect();
356    }
357}
358
359impl<Link> widget::operation::Text for State<Link>
360where
361    Link: Clone + 'static,
362{
363    fn text(&self) -> Fragment<'_> {
364        self.spans
365            .iter()
366            .fold(String::new(), |mut text, next| {
367                text.push_str(&next.text);
368                text
369            })
370            .into()
371    }
372
373    fn select(&mut self, start: Point, end: Point, target: Target) {
374        let bounds = core::Rectangle::INFINITE;
375
376        let start = {
377            let (line, index) = self
378                .grapheme_line_and_index(start, bounds)
379                .unwrap_or((0, 0));
380
381            SelectionEnd { line, index }
382        };
383
384        let end = {
385            let (line, index) =
386                self.grapheme_line_and_index(end, bounds).unwrap_or((0, 0));
387
388            SelectionEnd { line, index }
389        };
390
391        match target {
392            Target::Character => self.selection.select_range(start, end),
393            Target::Word => {
394                self.selection.select_word(
395                    start.line,
396                    start.index,
397                    &self.paragraph,
398                );
399
400                self.selection
401                    .change_selection_by_word(end, &self.paragraph);
402            }
403            Target::Line => {
404                self.selection.select_line(start.line, &self.paragraph);
405
406                self.selection
407                    .change_selection_by_line(end.line, &self.paragraph);
408            }
409        };
410    }
411
412    fn select_all(&mut self) {
413        self.selection.select_all(&self.paragraph);
414    }
415
416    fn deselect(&mut self) {
417        self.selection.clear();
418    }
419
420    fn copy(&mut self) -> Option<String> {
421        (!self.selection.is_empty())
422            .then(|| self.selection.text(&self.paragraph))
423    }
424}
425
426impl<Link, Message, Theme> widget::Meta for Rich<'_, Link, Message, Theme>
427where
428    Link: Clone + 'static,
429    Theme: Catalog,
430{
431}
432
433impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
434    for Rich<'_, Link, Message, Theme>
435where
436    Link: Clone + 'static,
437    Theme: Catalog,
438    Renderer: core::text::Renderer<Paragraph = Paragraph>,
439{
440    fn tag(&self) -> tree::Tag {
441        tree::Tag::of::<State<Link>>()
442    }
443
444    fn state(&self) -> tree::State {
445        tree::State::new(State::<Link> {
446            spans: Vec::new(),
447            span_pressed: None,
448            hovered_link: None,
449            paragraph: Paragraph::default(),
450            is_hovered: false,
451            selection: Selection::default(),
452            dragging: None,
453            last_click: None,
454            keyboard_modifiers: keyboard::Modifiers::default(),
455            visual_lines_bounds: Vec::new(),
456        })
457    }
458
459    fn size(&self) -> Size<Length> {
460        Size {
461            width: self.width,
462            height: self.height,
463        }
464    }
465
466    fn layout(
467        &mut self,
468        tree: &mut Tree,
469        renderer: &Renderer,
470        limits: &layout::Limits,
471    ) {
472        tree.size = layout(
473            tree.state.downcast_mut::<State<Link>>(),
474            renderer,
475            limits,
476            self.width,
477            self.height,
478            self.spans.as_ref().as_ref(),
479            self.line_height,
480            self.size,
481            self.font,
482            self.align_x,
483            self.align_y,
484            self.wrapping,
485            self.ellipsis,
486        );
487    }
488
489    fn draw(
490        &self,
491        tree: &Tree,
492        renderer: &mut Renderer,
493        theme: &Theme,
494        defaults: &renderer::Style,
495        layout: Layout,
496        _cursor: mouse::Cursor,
497        viewport: &Rectangle,
498    ) {
499        if !layout.bounds().intersects(viewport) {
500            return;
501        }
502
503        let state = tree.state.downcast_ref::<State<Link>>();
504
505        let style = theme.style(&self.class);
506
507        for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
508            let is_hovered_link = self.on_link_click.is_some()
509                && Some(index) == state.hovered_link;
510
511            if span.highlight.is_some()
512                || span.underline
513                || span.strikethrough
514                || is_hovered_link
515            {
516                let translation = layout.position() - Point::ORIGIN;
517                let regions = state.paragraph.span_bounds(index);
518
519                if let Some(highlight) = span.highlight {
520                    for (i, bounds) in regions.iter().enumerate() {
521                        let starts = i == 0;
522                        let ends = i + 1 == regions.len();
523
524                        let left = if starts { span.padding.left } else { 0.0 };
525                        let right = if ends { span.padding.right } else { 0.0 };
526
527                        let bounds = Rectangle::new(
528                            bounds.position()
529                                - Vector::new(left, span.padding.top),
530                            bounds.size()
531                                + Size::new(left + right, span.padding.y()),
532                        );
533
534                        let radius = border::Radius {
535                            top_left: highlight.border.radius.top_left
536                                * f32::from(starts),
537                            bottom_left: highlight.border.radius.bottom_left
538                                * f32::from(starts),
539                            top_right: highlight.border.radius.top_right
540                                * f32::from(ends),
541                            bottom_right: highlight.border.radius.bottom_right
542                                * f32::from(ends),
543                        };
544
545                        renderer.fill_quad(
546                            renderer::Quad {
547                                bounds: bounds + translation,
548                                border: Border {
549                                    radius,
550                                    ..highlight.border
551                                },
552                                ..Default::default()
553                            },
554                            highlight.background,
555                        );
556                    }
557                }
558
559                if span.underline || span.strikethrough || is_hovered_link {
560                    let size =
561                        span.size.or(self.size).unwrap_or(renderer.text_size());
562
563                    let line_height = span
564                        .line_height
565                        .or(self.line_height)
566                        .unwrap_or_else(|| renderer.line_height())
567                        .to_absolute(size);
568
569                    let color = span
570                        .color
571                        .or(style.color)
572                        .unwrap_or(defaults.text_color);
573
574                    let baseline = translation
575                        + Vector::new(
576                            0.0,
577                            size.0 + (line_height.0 - size.0) / 2.0,
578                        );
579
580                    if span.underline || is_hovered_link {
581                        for bounds in &regions {
582                            renderer.fill_quad(
583                                renderer::Quad {
584                                    bounds: Rectangle::new(
585                                        bounds.position() + baseline,
586                                        Size::new(bounds.width, 1.0),
587                                    ),
588                                    ..Default::default()
589                                },
590                                color,
591                            );
592                        }
593                    }
594
595                    if span.strikethrough {
596                        for bounds in &regions {
597                            renderer.fill_quad(
598                                renderer::Quad {
599                                    bounds: Rectangle::new(
600                                        bounds.position() + baseline
601                                            - Vector::new(0.0, size.0 / 2.0),
602                                        Size::new(bounds.width, 1.0),
603                                    ),
604                                    ..Default::default()
605                                },
606                                color,
607                            );
608                        }
609                    }
610                }
611            }
612        }
613
614        if !state.selection.is_empty() {
615            let bounds = layout.bounds();
616            let translation = bounds.position() - Point::ORIGIN;
617            let ranges = state.selection();
618
619            for range in ranges
620                .into_iter()
621                .filter_map(|range| bounds.intersection(&(range + translation)))
622            {
623                renderer.fill_quad(
624                    renderer::Quad {
625                        bounds: range,
626                        ..renderer::Quad::default()
627                    },
628                    style.selection,
629                );
630            }
631        }
632
633        crate::text::draw(
634            renderer,
635            defaults,
636            layout.bounds(),
637            &state.paragraph,
638            style,
639            viewport,
640        );
641    }
642
643    fn update(
644        &mut self,
645        tree: &mut Tree,
646        event: &Event,
647        layout: Layout,
648        cursor: mouse::Cursor,
649        _renderer: &Renderer,
650        shell: &mut Shell<'_, Message>,
651        viewport: &Rectangle,
652    ) {
653        let state = tree.state.downcast_mut::<State<Link>>();
654
655        let bounds = layout.bounds();
656        let click_position = cursor.position_in(bounds);
657
658        if viewport.intersection(&bounds).is_none()
659            && state.selection.is_empty()
660            && state.dragging.is_none()
661        {
662            return;
663        }
664
665        let was_hovered = state.is_hovered;
666        let hovered_link_before = state.hovered_link;
667        let selection_before = state.selection;
668
669        state.is_hovered = click_position.is_some();
670
671        if let Some(position) = click_position {
672            state.hovered_link =
673                state.paragraph.hit_span(position).filter(|span| {
674                    let Some(span) = self.spans.as_ref().as_ref().get(*span)
675                    else {
676                        return false;
677                    };
678
679                    span.link.is_some()
680                });
681        } else {
682            state.hovered_link = None;
683        }
684
685        match event {
686            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
687            | Event::Touch(touch::Event::FingerPressed { .. }) => {
688                if state.hovered_link.is_some() {
689                    state.span_pressed = state.hovered_link;
690                    shell.capture_event();
691                }
692
693                if let Some(position) = cursor.position_over(bounds) {
694                    let click = click::Click::new(
695                        position,
696                        mouse::Button::Left,
697                        state.last_click,
698                        self.click_interval,
699                    );
700
701                    let (line, index) = state
702                        .grapheme_line_and_index(position, bounds)
703                        .unwrap_or((0, 0));
704
705                    match click.kind() {
706                        click::Kind::Single => {
707                            let new_end = SelectionEnd { line, index };
708
709                            if state.keyboard_modifiers.shift() {
710                                state.selection.change_selection(new_end);
711                            } else {
712                                state.selection.select_range(new_end, new_end);
713                            }
714
715                            state.dragging = Some(Dragging::Grapheme);
716                        }
717                        click::Kind::Double => {
718                            state.selection.select_word(
719                                line,
720                                index,
721                                &state.paragraph,
722                            );
723                            state.dragging = Some(Dragging::Word);
724                        }
725                        click::Kind::Triple => {
726                            state.selection.select_line(line, &state.paragraph);
727                            state.dragging = Some(Dragging::Line);
728                        }
729                    }
730
731                    state.last_click = Some(click);
732
733                    shell.capture_event();
734                } else {
735                    state.selection = Selection::default();
736                }
737            }
738            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
739            | Event::Touch(touch::Event::FingerLifted { .. })
740            | Event::Touch(touch::Event::FingerLost { .. }) => {
741                state.dragging = None;
742                if !matches!(
743                    event,
744                    Event::Touch(touch::Event::FingerLost { .. })
745                ) && state.selection.is_empty()
746                {
747                    match state.span_pressed {
748                        Some(span) if Some(span) == state.hovered_link => {
749                            if let Some((link, on_link_clicked)) = self
750                                .spans
751                                .as_ref()
752                                .as_ref()
753                                .get(span)
754                                .and_then(|span| span.link.clone())
755                                .zip(self.on_link_click.as_deref())
756                            {
757                                shell.publish(on_link_clicked(link));
758                            }
759                        }
760                        _ => {}
761                    }
762
763                    state.span_pressed = None;
764                }
765            }
766            Event::Mouse(mouse::Event::CursorMoved { .. })
767            | Event::Touch(touch::Event::FingerMoved { .. }) => {
768                if let Some(position) = cursor.land().position()
769                    && let Some(dragging) = state.dragging
770                {
771                    let (line, index) = state
772                        .grapheme_line_and_index(position, bounds)
773                        .unwrap_or((0, 0));
774
775                    match dragging {
776                        Dragging::Grapheme => {
777                            let new_end = SelectionEnd { line, index };
778
779                            state.selection.change_selection(new_end);
780                        }
781                        Dragging::Word => {
782                            let new_end = SelectionEnd { line, index };
783
784                            state.selection.change_selection_by_word(
785                                new_end,
786                                &state.paragraph,
787                            );
788                        }
789                        Dragging::Line => {
790                            state.selection.change_selection_by_line(
791                                line,
792                                &state.paragraph,
793                            );
794                        }
795                    };
796                }
797            }
798            Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
799                match key.as_ref() {
800                    keyboard::Key::Character("c")
801                        if state.keyboard_modifiers.command()
802                            && !state.selection.is_empty() =>
803                    {
804                        shell.write_clipboard(
805                            state.selection.text(&state.paragraph),
806                        );
807
808                        shell.capture_event();
809                    }
810                    keyboard::Key::Character("a")
811                        if state.keyboard_modifiers.command()
812                            && !state.selection.is_empty() =>
813                    {
814                        state.selection.select_all(&state.paragraph);
815
816                        shell.capture_event();
817                    }
818                    keyboard::Key::Named(key::Named::Home)
819                        if state.keyboard_modifiers.shift()
820                            && !state.selection.is_empty() =>
821                    {
822                        if state.keyboard_modifiers.jump() {
823                            state.selection.select_beginning();
824                        } else {
825                            state.selection.select_line_beginning();
826                        }
827
828                        shell.capture_event();
829                    }
830                    keyboard::Key::Named(key::Named::End)
831                        if state.keyboard_modifiers.shift()
832                            && !state.selection.is_empty() =>
833                    {
834                        if state.keyboard_modifiers.jump() {
835                            state.selection.select_end(&state.paragraph);
836                        } else {
837                            state.selection.select_line_end(&state.paragraph);
838                        }
839
840                        shell.capture_event();
841                    }
842                    keyboard::Key::Named(key::Named::ArrowLeft)
843                        if state.keyboard_modifiers.shift()
844                            && !state.selection.is_empty() =>
845                    {
846                        if state.keyboard_modifiers.macos_command() {
847                            state.selection.select_line_beginning();
848                        } else if state.keyboard_modifiers.jump() {
849                            state
850                                .selection
851                                .select_left_by_words(&state.paragraph);
852                        } else {
853                            state.selection.select_left(&state.paragraph);
854                        }
855
856                        shell.capture_event();
857                    }
858                    keyboard::Key::Named(key::Named::ArrowRight)
859                        if state.keyboard_modifiers.shift()
860                            && !state.selection.is_empty() =>
861                    {
862                        if state.keyboard_modifiers.macos_command() {
863                            state.selection.select_line_end(&state.paragraph);
864                        } else if state.keyboard_modifiers.jump() {
865                            state
866                                .selection
867                                .select_right_by_words(&state.paragraph);
868                        } else {
869                            state.selection.select_right(&state.paragraph);
870                        }
871
872                        shell.capture_event();
873                    }
874                    keyboard::Key::Named(key::Named::ArrowUp)
875                        if state.keyboard_modifiers.shift()
876                            && !state.selection.is_empty() =>
877                    {
878                        if state.keyboard_modifiers.macos_command() {
879                            state.selection.select_beginning();
880                        } else if state.keyboard_modifiers.jump() {
881                            state.selection.select_line_beginning();
882                        } else {
883                            state.selection.select_up(&state.paragraph);
884                        }
885
886                        shell.capture_event();
887                    }
888                    keyboard::Key::Named(key::Named::ArrowDown)
889                        if state.keyboard_modifiers.shift()
890                            && !state.selection.is_empty() =>
891                    {
892                        if state.keyboard_modifiers.macos_command() {
893                            state.selection.select_end(&state.paragraph);
894                        } else if state.keyboard_modifiers.jump() {
895                            state.selection.select_line_end(&state.paragraph);
896                        } else {
897                            state.selection.select_down(&state.paragraph);
898                        }
899
900                        shell.capture_event();
901                    }
902                    keyboard::Key::Named(key::Named::Escape) => {
903                        state.dragging = None;
904                        state.selection = Selection::default();
905
906                        state.keyboard_modifiers =
907                            keyboard::Modifiers::default();
908
909                        if state.selection != selection_before {
910                            shell.capture_event();
911                        }
912                    }
913                    _ => {}
914                }
915            }
916            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
917                state.keyboard_modifiers = *modifiers;
918            }
919            _ => {}
920        }
921
922        if state.is_hovered != was_hovered
923            || state.selection != selection_before
924            || state.hovered_link != hovered_link_before
925        {
926            if let Some(span) = state.hovered_link {
927                if let Some((link, on_link_hovered)) = self
928                    .spans
929                    .as_ref()
930                    .as_ref()
931                    .get(span)
932                    .and_then(|span| span.link.clone())
933                    .zip(self.on_link_hover.as_deref())
934                {
935                    shell.publish(on_link_hovered(link));
936                }
937            } else if let Some(on_hover_lost) = self.on_hover_lost.as_deref() {
938                shell.publish(on_hover_lost());
939            }
940
941            shell.request_redraw();
942        }
943    }
944
945    fn mouse_interaction(
946        &self,
947        tree: &Tree,
948        layout: Layout,
949        cursor: mouse::Cursor,
950        _viewport: &Rectangle,
951        _renderer: &Renderer,
952    ) -> mouse::Interaction {
953        let state = tree.state.downcast_ref::<State<Link>>();
954
955        if state.hovered_link.is_some() {
956            mouse::Interaction::Pointer
957        } else if cursor.is_over(layout.bounds()) || state.dragging.is_some() {
958            mouse::Interaction::Text
959        } else {
960            mouse::Interaction::None
961        }
962    }
963}
964
965#[allow(clippy::too_many_arguments)]
966fn layout<Link, Renderer>(
967    state: &mut State<Link>,
968    renderer: &Renderer,
969    limits: &layout::Limits,
970    width: Length,
971    height: Length,
972    spans: &[Span<'_, Link>],
973    line_height: Option<LineHeight>,
974    size: Option<Pixels>,
975    font: Option<Font>,
976    align_x: Alignment,
977    align_y: alignment::Vertical,
978    wrapping: Wrapping,
979    ellipsis: Ellipsis,
980) -> Size
981where
982    Link: Clone,
983    Renderer: core::text::Renderer<Paragraph = Paragraph>,
984{
985    layout::sized(limits, width, height, |limits| {
986        let bounds = limits.bounds();
987
988        let size = size.unwrap_or_else(|| renderer.text_size());
989        let font = font.unwrap_or_else(|| renderer.font());
990        let line_height = line_height.unwrap_or_else(|| renderer.line_height());
991
992        let text_with_spans = || core::Text {
993            content: spans,
994            bounds,
995            size,
996            line_height,
997            font,
998            align_x,
999            align_y,
1000            shaping: Shaping::Advanced,
1001            wrapping,
1002            ellipsis,
1003            hint_factor: renderer.hint_factor(),
1004        };
1005
1006        if state.spans != spans {
1007            state.paragraph =
1008                Renderer::Paragraph::with_spans(text_with_spans());
1009            state.spans = spans.iter().cloned().map(Span::to_static).collect();
1010            state.update_visual_bounds();
1011        } else {
1012            match state.paragraph.compare(core::Text {
1013                content: (),
1014                bounds,
1015                size,
1016                line_height,
1017                font,
1018                align_x,
1019                align_y,
1020                shaping: Shaping::Advanced,
1021                wrapping,
1022                ellipsis,
1023                hint_factor: renderer.hint_factor(),
1024            }) {
1025                core::text::Difference::None => {}
1026                core::text::Difference::Bounds => {
1027                    state.paragraph.resize(bounds);
1028                    state.update_visual_bounds();
1029                }
1030                core::text::Difference::Shape => {
1031                    state.paragraph =
1032                        Renderer::Paragraph::with_spans(text_with_spans());
1033                    state.update_visual_bounds();
1034                }
1035            }
1036        }
1037
1038        state.paragraph.min_bounds()
1039    })
1040}
1041
1042impl<'a, Link, Message, Theme> FromIterator<Span<'a, Link>>
1043    for Rich<'a, Link, Message, Theme>
1044where
1045    Link: Clone + 'a,
1046    Theme: Catalog,
1047{
1048    fn from_iter<T: IntoIterator<Item = Span<'a, Link>>>(spans: T) -> Self {
1049        Self::with_spans(spans.into_iter().collect::<Vec<_>>())
1050    }
1051}
1052
1053fn highlight_line(
1054    line: &cosmic_text::BufferLine,
1055    from: usize,
1056    to: usize,
1057) -> impl Iterator<Item = (f32, f32)> + '_ {
1058    let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default();
1059
1060    // Check for multi codepoint glyphs for each previous visual line
1061    let mut previous_diff = 0;
1062    let previous_lines_diff = line
1063        .layout_opt()
1064        .map(Vec::as_slice)
1065        .unwrap_or_default()
1066        .iter()
1067        .enumerate()
1068        .map(move |(line_nr, visual_line)| {
1069            if line_nr == 0 {
1070                let current_diff = previous_diff
1071                    + visual_line.glyphs.iter().fold(0, |d, g| {
1072                        (d + g.start.abs_diff(g.end)).saturating_sub(1)
1073                    });
1074                previous_diff = current_diff;
1075                0
1076            } else {
1077                let current_diff = previous_diff
1078                    + visual_line
1079                        .glyphs
1080                        .iter()
1081                        .fold(0, |d, g| d + g.start.abs_diff(g.end) - 1);
1082                let previous_diff_temp = previous_diff;
1083                previous_diff = current_diff;
1084                previous_diff_temp
1085            }
1086        });
1087
1088    layout.iter().zip(previous_lines_diff).map(
1089        move |(visual_line, previous_lines_diff)| {
1090            let start = visual_line
1091                .glyphs
1092                .first()
1093                .map(|glyph| glyph.start)
1094                .unwrap_or(0);
1095            let end = visual_line
1096                .glyphs
1097                .last()
1098                .map(|glyph| glyph.end)
1099                .unwrap_or(0);
1100
1101            let to = to + previous_lines_diff;
1102            let mut range = start.max(from)..end.min(to);
1103
1104            let x_offset = visual_line
1105                .glyphs
1106                .first()
1107                .map(|glyph| glyph.x)
1108                .unwrap_or_default();
1109
1110            if range.is_empty() {
1111                (x_offset, 0.0)
1112            } else if range.start == start && range.end == end {
1113                (x_offset, visual_line.w)
1114            } else {
1115                let mut x = 0.0;
1116                let mut width = 0.0;
1117                for glyph in &visual_line.glyphs {
1118                    let glyph_count = glyph.start.abs_diff(glyph.end);
1119
1120                    // Check for multi codepoint glyphs before or within the range
1121                    if glyph_count > 1 {
1122                        if range.start > glyph.start {
1123                            range.start += glyph_count - 1;
1124                            range.end += glyph_count - 1;
1125                        } else if range.end > glyph.start {
1126                            range.end += glyph_count - 1;
1127                        }
1128                    }
1129
1130                    if range.start > glyph.start {
1131                        x += glyph.w;
1132                    }
1133
1134                    if range.start <= glyph.start && range.end > glyph.start {
1135                        width += glyph.w;
1136                    } else if range.end <= glyph.start {
1137                        break;
1138                    }
1139                }
1140
1141                (x_offset + x, width)
1142            }
1143        },
1144    )
1145}
1146
1147fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
1148    let scroll = buffer.scroll();
1149
1150    let start = scroll.line.min(line);
1151    let end = scroll.line.max(line);
1152
1153    let visual_lines_offset: usize = buffer.lines[start..]
1154        .iter()
1155        .take(end - start)
1156        .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
1157        .sum();
1158
1159    visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
1160}