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
22pub 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 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 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 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
88 self.size = Some(size.into());
89 self
90 }
91
92 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
94 self.line_height = line_height.into();
95 self
96 }
97
98 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
100 self.font = Some(font.into());
101 self
102 }
103
104 pub fn width(mut self, width: impl Into<Length>) -> Self {
106 self.width = width.into();
107 self
108 }
109
110 pub fn height(mut self, height: impl Into<Length>) -> Self {
112 self.height = height.into();
113 self
114 }
115
116 pub fn center(self) -> Self {
118 self.align_x(alignment::Horizontal::Center)
119 .align_y(alignment::Vertical::Center)
120 }
121
122 pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
124 self.align_x = alignment.into();
125 self
126 }
127
128 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 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
139 self.wrapping = wrapping;
140 self
141 }
142
143 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 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 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 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 #[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 #[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 ®ions {
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 ®ions {
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 ®ions {
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 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 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}