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