Skip to main content

iced_graphics/text/
paragraph.rs

1//! Draw paragraphs.
2use crate::core;
3use crate::core::alignment;
4use crate::core::text::{
5    Alignment, Ellipsis, Hit, LineHeight, Shaping, Span, Target, Text, Wrapping,
6};
7use crate::core::{Font, Pixels, Point, Rectangle, Size};
8use crate::text;
9
10use std::fmt;
11use std::sync::{self, Arc};
12
13/// A bunch of text.
14#[derive(Clone, PartialEq)]
15pub struct Paragraph(Arc<Internal>);
16
17#[derive(Clone)]
18struct Internal {
19    buffer: cosmic_text::Buffer,
20    font: Font,
21    shaping: Shaping,
22    wrapping: Wrapping,
23    ellipsis: Ellipsis,
24    align_x: Alignment,
25    align_y: alignment::Vertical,
26    bounds: Size,
27    min_bounds: Size,
28    version: text::Version,
29    hint: bool,
30    hint_factor: f32,
31    selection: Option<Selection>,
32}
33
34#[derive(Clone)]
35struct Selection {
36    start: cosmic_text::Cursor,
37    end: cosmic_text::Cursor,
38    regions: Vec<Rectangle>,
39}
40
41impl Paragraph {
42    /// Creates a new empty [`Paragraph`].
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Returns the buffer of the [`Paragraph`].
48    pub fn buffer(&self) -> &cosmic_text::Buffer {
49        &self.internal().buffer
50    }
51
52    /// Creates a [`Weak`] reference to the [`Paragraph`].
53    ///
54    /// This is useful to avoid cloning the [`Paragraph`] when
55    /// referential guarantees are unnecessary. For instance,
56    /// when creating a rendering tree.
57    pub fn downgrade(&self) -> Weak {
58        let paragraph = self.internal();
59
60        Weak {
61            raw: Arc::downgrade(paragraph),
62            min_bounds: paragraph.min_bounds,
63            align_x: paragraph.align_x,
64            align_y: paragraph.align_y,
65        }
66    }
67
68    fn internal(&self) -> &Arc<Internal> {
69        &self.0
70    }
71}
72
73impl core::text::Paragraph for Paragraph {
74    fn with_text(text: Text<&str>) -> Self {
75        log::trace!("Allocating plain paragraph: {}", text.content);
76
77        let mut font_system = text::font_system().write().expect("Write font system");
78
79        let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
80            Some(hint_factor) => (true, hint_factor),
81            _ => (false, 1.0),
82        };
83
84        let mut buffer = cosmic_text::Buffer::new(
85            font_system.raw(),
86            cosmic_text::Metrics::new(
87                f32::from(text.size) * hint_factor,
88                f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
89            ),
90        );
91
92        if hint {
93            buffer.set_hinting(cosmic_text::Hinting::Enabled);
94        }
95
96        buffer.set_size(
97            Some(text.bounds.width * hint_factor),
98            Some(text.bounds.height * hint_factor),
99        );
100
101        buffer.set_wrap(text::to_wrap(text.wrapping));
102        buffer.set_ellipsize(text::to_ellipsize(
103            text.ellipsis,
104            text.bounds.height * hint_factor,
105        ));
106
107        buffer.set_text(
108            text.content,
109            &text::to_attributes(text.font),
110            text::to_shaping(text.shaping, text.content),
111            None,
112        );
113        buffer.shape_until_scroll(font_system.raw(), false);
114
115        let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
116
117        Self(Arc::new(Internal {
118            buffer,
119            hint,
120            hint_factor,
121            font: text.font,
122            align_x: text.align_x,
123            align_y: text.align_y,
124            shaping: text.shaping,
125            wrapping: text.wrapping,
126            ellipsis: text.ellipsis,
127            bounds: text.bounds,
128            min_bounds,
129            version: font_system.version(),
130            selection: None,
131        }))
132    }
133
134    fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
135        log::trace!("Allocating rich paragraph: {} spans", text.content.len());
136
137        let mut font_system = text::font_system().write().expect("Write font system");
138
139        let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
140            Some(hint_factor) => (true, hint_factor),
141            _ => (false, 1.0),
142        };
143
144        let mut buffer = cosmic_text::Buffer::new(
145            font_system.raw(),
146            cosmic_text::Metrics::new(
147                f32::from(text.size) * hint_factor,
148                f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
149            ),
150        );
151
152        if hint {
153            buffer.set_hinting(cosmic_text::Hinting::Enabled);
154        }
155
156        buffer.set_size(
157            Some(text.bounds.width * hint_factor),
158            Some(text.bounds.height * hint_factor),
159        );
160
161        buffer.set_wrap(text::to_wrap(text.wrapping));
162
163        buffer.set_rich_text(
164            text.content.iter().enumerate().map(|(i, span)| {
165                let attrs = text::to_attributes(span.font.unwrap_or(text.font));
166
167                let attrs = match (span.size, span.line_height) {
168                    (None, None) => attrs,
169                    _ => {
170                        let size = span.size.unwrap_or(text.size);
171
172                        attrs.metrics(cosmic_text::Metrics::new(
173                            f32::from(size) * hint_factor,
174                            f32::from(
175                                span.line_height
176                                    .unwrap_or(text.line_height)
177                                    .to_absolute(size),
178                            ) * hint_factor,
179                        ))
180                    }
181                };
182
183                let attrs = if let Some(color) = span.color {
184                    attrs.color(text::to_color(color))
185                } else {
186                    attrs
187                };
188
189                let attrs = attrs.padding(cosmic_text::SpanPadding {
190                    start: span.padding.left,
191                    end: span.padding.right,
192                });
193
194                (span.text.as_ref(), attrs.metadata(i))
195            }),
196            &text::to_attributes(text.font),
197            cosmic_text::Shaping::Advanced,
198            None,
199        );
200
201        buffer.shape_until_scroll(font_system.raw(), false);
202
203        let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
204
205        Self(Arc::new(Internal {
206            buffer,
207            hint,
208            hint_factor,
209            font: text.font,
210            align_x: text.align_x,
211            align_y: text.align_y,
212            shaping: text.shaping,
213            wrapping: text.wrapping,
214            ellipsis: text.ellipsis,
215            bounds: text.bounds,
216            min_bounds,
217            version: font_system.version(),
218            selection: None,
219        }))
220    }
221
222    fn resize(&mut self, new_bounds: Size) {
223        let paragraph = Arc::make_mut(&mut self.0);
224
225        let mut font_system = text::font_system().write().expect("Write font system");
226
227        paragraph.buffer.set_size(
228            Some(new_bounds.width * paragraph.hint_factor),
229            Some(new_bounds.height * paragraph.hint_factor),
230        );
231        paragraph
232            .buffer
233            .shape_until_scroll(font_system.raw(), false);
234
235        let min_bounds = text::align(&mut paragraph.buffer, font_system.raw(), paragraph.align_x)
236            / paragraph.hint_factor;
237
238        paragraph.bounds = new_bounds;
239        paragraph.min_bounds = min_bounds;
240
241        if let Some(Selection { start, end, .. }) = paragraph.selection {
242            paragraph.selection = Some(Selection {
243                start,
244                end,
245                regions: text::regions(&paragraph.buffer, start, end),
246            });
247        }
248    }
249
250    fn compare(&self, text: Text<()>) -> core::text::Difference {
251        let font_system = text::font_system().read().expect("Read font system");
252        let paragraph = self.internal();
253        let metrics = paragraph.buffer.metrics();
254
255        if paragraph.version != font_system.version
256            || metrics.font_size != text.size.0 * paragraph.hint_factor
257            || metrics.line_height
258                != text.line_height.to_absolute(text.size).0 * paragraph.hint_factor
259            || paragraph.font != text.font
260            || paragraph.shaping != text.shaping
261            || paragraph.wrapping != text.wrapping
262            || paragraph.ellipsis != text.ellipsis
263            || paragraph.align_x != text.align_x
264            || paragraph.align_y != text.align_y
265            || paragraph.hint.then_some(paragraph.hint_factor)
266                != text::hint_factor(text.size, text.hint_factor)
267        {
268            core::text::Difference::Shape
269        } else if paragraph.bounds != text.bounds {
270            core::text::Difference::Bounds
271        } else {
272            core::text::Difference::None
273        }
274    }
275
276    fn hint_factor(&self) -> Option<f32> {
277        self.0.hint.then_some(self.0.hint_factor)
278    }
279
280    fn size(&self) -> Pixels {
281        Pixels(self.0.buffer.metrics().font_size / self.0.hint_factor)
282    }
283
284    fn font(&self) -> Font {
285        self.0.font
286    }
287
288    fn line_height(&self) -> LineHeight {
289        LineHeight::Absolute(Pixels(
290            self.0.buffer.metrics().line_height / self.0.hint_factor,
291        ))
292    }
293
294    fn align_x(&self) -> Alignment {
295        self.internal().align_x
296    }
297
298    fn align_y(&self) -> alignment::Vertical {
299        self.internal().align_y
300    }
301
302    fn wrapping(&self) -> Wrapping {
303        self.0.wrapping
304    }
305
306    fn ellipsis(&self) -> Ellipsis {
307        self.0.ellipsis
308    }
309
310    fn shaping(&self) -> Shaping {
311        self.0.shaping
312    }
313
314    fn bounds(&self) -> Size {
315        self.0.bounds
316    }
317
318    fn min_bounds(&self) -> Size {
319        self.internal().min_bounds
320    }
321
322    fn hit_test(&self, point: Point) -> Option<Hit> {
323        let cursor = self
324            .internal()
325            .buffer
326            .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
327
328        Some(Hit {
329            line: cursor.line,
330            index: cursor.index,
331        })
332    }
333
334    fn hit_glyph(&self, point: Point) -> bool {
335        self.internal().buffer.layout_runs().any(|run| {
336            let line_top = run.line_top;
337            let line_height = run.line_height;
338
339            if line_top > point.y || line_top + line_height < point.y {
340                return false;
341            }
342
343            run.glyphs
344                .iter()
345                .any(|glyph| glyph.x <= point.x && glyph.x + glyph.w >= point.x)
346        })
347    }
348
349    fn hit_span(&self, point: Point) -> Option<usize> {
350        let internal = self.internal();
351
352        let cursor = internal
353            .buffer
354            .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
355        let line = internal.buffer.lines.get(cursor.line)?;
356
357        if cursor.index >= line.text().len() {
358            return None;
359        }
360
361        let index = match cursor.affinity {
362            cosmic_text::Affinity::Before => cursor.index.saturating_sub(1),
363            cosmic_text::Affinity::After => cursor.index,
364        };
365
366        let mut hit = None;
367        let glyphs = line
368            .layout_opt()
369            .as_ref()?
370            .iter()
371            .flat_map(|line| line.glyphs.iter());
372
373        for glyph in glyphs {
374            if glyph.start <= index && index < glyph.end {
375                hit = Some(glyph);
376                break;
377            }
378        }
379
380        Some(hit?.metadata)
381    }
382
383    fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
384        let internal = self.internal();
385
386        let scale = 1.0 / internal.hint_factor;
387
388        let mut bounds = Vec::new();
389        let mut current = None;
390        let mut current_baseline = 0.0;
391
392        let mut y = 0.0;
393        let buffer_height = internal.buffer.metrics().line_height;
394        let glyphs = internal
395            .buffer
396            .lines
397            .iter()
398            .filter_map(|paragraph| paragraph.layout_opt().map(Vec::as_slice))
399            .flat_map(|lines| lines.iter())
400            .flat_map(move |line| {
401                let line_height = line.line_height(buffer_height);
402                let ink_height = line.max_ascent + line.max_descent;
403
404                // The renderer centers the line's ink within the line
405                // height and places the baseline `max_ascent` below the
406                // top of the ink:
407                let baseline = y + (line_height - ink_height) / 2.0 + line.max_ascent;
408
409                let glyphs = line.glyphs.iter().map(move |glyph| (baseline, glyph));
410
411                y += line_height;
412
413                glyphs
414            })
415            .skip_while(|(_, glyph)| glyph.metadata != index)
416            .take_while(|(_, glyph)| glyph.metadata == index);
417
418        for (baseline, glyph) in glyphs {
419            // Glyphs can be offset from the line's baseline (e.g. by complex
420            // scripts); mirror the anchor used in `LayoutGlyph::physical`:
421            let anchor = baseline + glyph.y - glyph.font_size * glyph.y_offset;
422            let ink_top = anchor - glyph.ascender;
423            let ink_bottom = anchor + glyph.descender;
424
425            match current.as_mut() {
426                None => {
427                    current_baseline = baseline;
428                    current = Some(
429                        Rectangle::new(
430                            Point::new(glyph.x, ink_top),
431                            Size::new(glyph.w, ink_bottom - ink_top),
432                        ) * scale,
433                    );
434                }
435                Some(current) if baseline != current_baseline => {
436                    bounds.push(*current);
437                    current_baseline = baseline;
438                    *current = Rectangle::new(
439                        Point::new(glyph.x, ink_top),
440                        Size::new(glyph.w, ink_bottom - ink_top),
441                    ) * scale;
442                }
443                Some(current) => {
444                    // Union the glyph's ink with the span's bounds on this
445                    // line:
446                    let left = current.x.min(glyph.x * scale);
447                    let top = current.y.min(ink_top * scale);
448                    let right = (current.x + current.width).max((glyph.x + glyph.w) * scale);
449                    let bottom = (current.y + current.height).max(ink_bottom * scale);
450                    *current = Rectangle::new(
451                        Point::new(left, top),
452                        Size::new(right - left, bottom - top),
453                    );
454                }
455            }
456        }
457
458        bounds.extend(current);
459        bounds
460    }
461
462    fn select(&mut self, start: Point, end: Point, target: Target) {
463        use cosmic_text::Edit;
464
465        let paragraph = Arc::make_mut(&mut self.0);
466        let mut font_system = text::font_system().write().expect("write font system");
467
468        // Simulate a selection drag
469        let mut editor =
470            cosmic_text::Editor::new(cosmic_text::BufferRef::Borrowed(&mut paragraph.buffer));
471
472        editor.action(&mut font_system.raw, {
473            let x = start.x as i32;
474            let y = start.y as i32;
475
476            match target {
477                Target::Character => cosmic_text::Action::Click { x, y },
478                Target::Word => cosmic_text::Action::DoubleClick { x, y },
479                Target::Line => cosmic_text::Action::TripleClick { x, y },
480            }
481        });
482
483        editor.action(
484            &mut font_system.raw,
485            cosmic_text::Action::Drag {
486                x: end.x as i32,
487                y: end.y as i32,
488            },
489        );
490
491        let Some((start, end)) = editor.selection_bounds() else {
492            paragraph.selection = None;
493            return;
494        };
495
496        let cursor = editor.cursor();
497
498        let selection = match editor.selection() {
499            cosmic_text::Selection::None => cursor,
500            cosmic_text::Selection::Normal(selection)
501            | cosmic_text::Selection::Line(selection)
502            | cosmic_text::Selection::Word(selection) => selection,
503        };
504
505        // If the cursor and selection both fall at either edge of the paragraph,
506        // nullify the selection.
507        if cursor == selection
508            && ((cursor.line == 0 && cursor.index == 0)
509                || (cursor.line + 1 == paragraph.buffer.lines.len()
510                    && cursor.index == paragraph.buffer.lines[cursor.line].text().len()))
511        {
512            paragraph.selection = None;
513            return;
514        }
515
516        paragraph.selection = Some(Selection {
517            start,
518            end,
519            regions: text::regions(&paragraph.buffer, start, end),
520        });
521    }
522
523    fn select_all(&mut self) {
524        let paragraph = Arc::make_mut(&mut self.0);
525
526        let start = cosmic_text::Cursor {
527            line: 0,
528            index: 0,
529            affinity: cosmic_text::Affinity::Before,
530        };
531
532        let end = cosmic_text::Cursor {
533            line: paragraph.buffer.lines.len().saturating_sub(1),
534            index: paragraph
535                .buffer
536                .lines
537                .last()
538                .map(|line| line.text().floor_char_boundary(line.text().len()))
539                .unwrap_or_default(),
540            affinity: cosmic_text::Affinity::After,
541        };
542
543        paragraph.selection = Some(Selection {
544            start,
545            end,
546            regions: text::regions(&paragraph.buffer, start, end),
547        });
548    }
549
550    fn deselect(&mut self) {
551        let paragraph = Arc::make_mut(&mut self.0);
552        paragraph.selection = None;
553    }
554
555    fn selection(&self) -> &[Rectangle] {
556        self.internal()
557            .selection
558            .as_ref()
559            .map(|selection| selection.regions.as_slice())
560            .unwrap_or_default()
561    }
562
563    fn copy(&mut self) -> Option<String> {
564        use cosmic_text::Edit;
565
566        let paragraph = Arc::make_mut(&mut self.0);
567        let selection = paragraph.selection.as_ref()?;
568
569        let mut editor =
570            cosmic_text::Editor::new(cosmic_text::BufferRef::Borrowed(&mut paragraph.buffer));
571
572        editor.set_selection(cosmic_text::Selection::Normal(selection.start));
573        editor.set_cursor(selection.end);
574        editor.copy_selection()
575    }
576}
577
578impl Default for Paragraph {
579    fn default() -> Self {
580        Self(Arc::new(Internal::default()))
581    }
582}
583
584impl fmt::Debug for Paragraph {
585    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586        let paragraph = self.internal();
587
588        f.debug_struct("Paragraph")
589            .field("font", &paragraph.font)
590            .field("shaping", &paragraph.shaping)
591            .field("horizontal_alignment", &paragraph.align_x)
592            .field("vertical_alignment", &paragraph.align_y)
593            .field("bounds", &paragraph.bounds)
594            .field("min_bounds", &paragraph.min_bounds)
595            .finish()
596    }
597}
598
599impl PartialEq for Internal {
600    fn eq(&self, other: &Self) -> bool {
601        self.font == other.font
602            && self.shaping == other.shaping
603            && self.align_x == other.align_x
604            && self.align_y == other.align_y
605            && self.bounds == other.bounds
606            && self.min_bounds == other.min_bounds
607            && self.buffer.metrics() == other.buffer.metrics()
608    }
609}
610
611impl Default for Internal {
612    fn default() -> Self {
613        Self {
614            buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
615                font_size: 1.0,
616                line_height: 1.0,
617            }),
618            font: Font::default(),
619            shaping: Shaping::default(),
620            wrapping: Wrapping::default(),
621            ellipsis: Ellipsis::default(),
622            align_x: Alignment::Default,
623            align_y: alignment::Vertical::Top,
624            bounds: Size::ZERO,
625            min_bounds: Size::ZERO,
626            version: text::Version::default(),
627            hint: false,
628            hint_factor: 1.0,
629            selection: None,
630        }
631    }
632}
633
634/// A weak reference to a [`Paragraph`].
635#[derive(Debug, Clone)]
636pub struct Weak {
637    raw: sync::Weak<Internal>,
638    /// The minimum bounds of the [`Paragraph`].
639    pub min_bounds: Size,
640    /// The horizontal alignment of the [`Paragraph`].
641    pub align_x: Alignment,
642    /// The vertical alignment of the [`Paragraph`].
643    pub align_y: alignment::Vertical,
644}
645
646impl Weak {
647    /// Tries to update the reference into a [`Paragraph`].
648    pub fn upgrade(&self) -> Option<Paragraph> {
649        self.raw.upgrade().map(Paragraph)
650    }
651}
652
653impl PartialEq for Weak {
654    fn eq(&self, other: &Self) -> bool {
655        match (self.raw.upgrade(), other.raw.upgrade()) {
656            (Some(p1), Some(p2)) => p1 == p2,
657            _ => false,
658        }
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::core::text::Paragraph as _;
666
667    fn rich_paragraph(content: &[Span<'_, ()>]) -> Paragraph {
668        let text = Text {
669            content,
670            bounds: Size::new(1000.0, f32::INFINITY),
671            size: Pixels(20.0),
672            line_height: LineHeight::Relative(1.5),
673            font: Font::default(),
674            align_x: Alignment::Default,
675            align_y: alignment::Vertical::Top,
676            shaping: Shaping::default(),
677            wrapping: Wrapping::default(),
678            ellipsis: Ellipsis::default(),
679            hint_factor: None,
680        };
681
682        Paragraph::with_spans(text)
683    }
684
685    /// Bounds should cover the ink of the span, without stretching to the
686    /// line height.
687    #[test]
688    fn span_bounds_cover_ink() {
689        let paragraph = rich_paragraph(&[Span::new("a"), Span::new("a").size(40.0)]);
690
691        let small = paragraph.span_bounds(0);
692        let big = paragraph.span_bounds(1);
693
694        assert_eq!(small.len(), 1);
695        assert_eq!(big.len(), 1);
696
697        let buffer = paragraph.buffer();
698        let line = &buffer.lines[0].layout_opt().unwrap()[0];
699        let line_height = line.line_height(buffer.metrics().line_height);
700        let ink_height = line.max_ascent + line.max_descent;
701        let centering = (line_height - ink_height) / 2.0;
702
703        // The larger span determines the line's ink:
704        assert!((big[0].y - centering).abs() < 1e-2);
705        assert!((big[0].height - ink_height).abs() < 1e-2);
706
707        // Both spans share the line's baseline:
708        let baseline = centering + line.max_ascent;
709        assert!((small[0].y + line.max_ascent / 2.0 - baseline).abs() < 1e-2);
710        assert!((big[0].y + line.max_ascent - baseline).abs() < 1e-2);
711
712        // The smaller span is strictly inside the line's ink box...
713        assert!(small[0].y > centering);
714        assert!(small[0].y + small[0].height < centering + ink_height);
715        assert!((small[0].height - ink_height / 2.0).abs() < 1e-2);
716
717        // ...and the ink stays inside the line box:
718        if ink_height < line_height {
719            assert!(big[0].height < line_height);
720        }
721    }
722
723    /// Bounds should cover the horizontal extent of the span's glyphs.
724    #[test]
725    fn span_bounds_cover_width() {
726        let paragraph = rich_paragraph(&[Span::new("Hello"), Span::new(", world!")]);
727
728        let first = paragraph.span_bounds(0);
729        let second = paragraph.span_bounds(1);
730
731        assert_eq!(first.len(), 1);
732        assert_eq!(second.len(), 1);
733
734        let buffer = paragraph.buffer();
735        let line = &buffer.lines[0].layout_opt().unwrap()[0];
736
737        // The first span starts at the beginning of the line and is a
738        // proper prefix of it:
739        assert!((first[0].x - 0.0).abs() < 1e-2);
740        assert!(first[0].width > 0.0);
741        assert!(first[0].width < line.w);
742
743        // The second span starts where the first span ends, and ends at
744        // the end of the line:
745        assert!((second[0].x - (first[0].x + first[0].width)).abs() < 1e-2);
746        assert!((second[0].x + second[0].width - line.w).abs() < 1e-2);
747    }
748
749    /// A span on multiple lines produces one bounds per line.
750    #[test]
751    fn span_bounds_cover_each_line() {
752        let paragraph = rich_paragraph(&[Span::new("ab\ncd")]);
753
754        let bounds = paragraph.span_bounds(0);
755
756        assert_eq!(bounds.len(), 2);
757
758        let buffer = paragraph.buffer();
759        let line_height = buffer.metrics().line_height;
760
761        for bounds in &bounds {
762            assert!((bounds.x - 0.0).abs() < 1e-2);
763            assert!(bounds.height > 0.0);
764            assert!(bounds.height < line_height);
765        }
766
767        // The lines are stacked without overlap, and both lines have the
768        // same ink height:
769        assert!(bounds[0].y + bounds[0].height <= bounds[1].y + 1e-2);
770        assert!((bounds[0].height - bounds[1].height).abs() < 1e-2);
771    }
772}