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#[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 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 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 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
85 self.size = Some(size.into());
86 self
87 }
88
89 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 pub fn font(mut self, font: impl Into<Font>) -> Self {
97 self.font = Some(font.into());
98 self
99 }
100
101 pub fn font_maybe(mut self, font: impl Into<Option<Font>>) -> Self {
103 self.font = font.into();
104 self
105 }
106
107 pub fn width(mut self, width: impl Into<Length>) -> Self {
109 self.width = width.into();
110 self
111 }
112
113 pub fn height(mut self, height: impl Into<Length>) -> Self {
115 self.height = height.into();
116 self
117 }
118
119 pub fn center(self) -> Self {
121 self.align_x(alignment::Horizontal::Center)
122 .align_y(alignment::Vertical::Center)
123 }
124
125 pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
127 self.align_x = alignment.into();
128 self
129 }
130
131 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 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
142 self.wrapping = wrapping;
143 self
144 }
145
146 pub fn ellipsis(mut self, ellipsis: Ellipsis) -> Self {
148 self.ellipsis = ellipsis;
149 self
150 }
151
152 pub fn click_interval(mut self, click_interval: Duration) -> Self {
157 self.click_interval = Some(click_interval);
158 self
159 }
160
161 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 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 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 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 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 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 ®ions {
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 ®ions {
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 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 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}