iced_selection/text/
rich.rs

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