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