Skip to main content

iced_graphics/text/
paragraph.rs

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