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    type Font = Font;
65
66    fn with_text(text: Text<&str>) -> Self {
67        log::trace!("Allocating plain paragraph: {}", text.content);
68
69        let mut font_system = text::font_system().write().expect("Write font system");
70
71        let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
72            Some(hint_factor) => (true, hint_factor),
73            _ => (false, 1.0),
74        };
75
76        let mut buffer = cosmic_text::Buffer::new(
77            font_system.raw(),
78            cosmic_text::Metrics::new(
79                f32::from(text.size) * hint_factor,
80                f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
81            ),
82        );
83
84        if hint {
85            buffer.set_hinting(font_system.raw(), cosmic_text::Hinting::Enabled);
86        }
87
88        buffer.set_size(
89            font_system.raw(),
90            Some(text.bounds.width * hint_factor),
91            Some(text.bounds.height * hint_factor),
92        );
93
94        buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping));
95        buffer.set_ellipsize(
96            font_system.raw(),
97            text::to_ellipsize(text.ellipsis, text.bounds.height * hint_factor),
98        );
99
100        buffer.set_text(
101            font_system.raw(),
102            text.content,
103            &text::to_attributes(text.font),
104            text::to_shaping(text.shaping, text.content),
105            None,
106        );
107
108        let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
109
110        Self(Arc::new(Internal {
111            buffer,
112            hint,
113            hint_factor,
114            font: text.font,
115            align_x: text.align_x,
116            align_y: text.align_y,
117            shaping: text.shaping,
118            wrapping: text.wrapping,
119            ellipsis: text.ellipsis,
120            bounds: text.bounds,
121            min_bounds,
122            version: font_system.version(),
123        }))
124    }
125
126    fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
127        log::trace!("Allocating rich paragraph: {} spans", text.content.len());
128
129        let mut font_system = text::font_system().write().expect("Write font system");
130
131        let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
132            Some(hint_factor) => (true, hint_factor),
133            _ => (false, 1.0),
134        };
135
136        let mut buffer = cosmic_text::Buffer::new(
137            font_system.raw(),
138            cosmic_text::Metrics::new(
139                f32::from(text.size) * hint_factor,
140                f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
141            ),
142        );
143
144        if hint {
145            buffer.set_hinting(font_system.raw(), cosmic_text::Hinting::Enabled);
146        }
147
148        buffer.set_size(
149            font_system.raw(),
150            Some(text.bounds.width * hint_factor),
151            Some(text.bounds.height * hint_factor),
152        );
153
154        buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping));
155
156        buffer.set_rich_text(
157            font_system.raw(),
158            text.content.iter().enumerate().map(|(i, span)| {
159                let attrs = text::to_attributes(span.font.unwrap_or(text.font));
160
161                let attrs = match (span.size, span.line_height) {
162                    (None, None) => attrs,
163                    _ => {
164                        let size = span.size.unwrap_or(text.size);
165
166                        attrs.metrics(cosmic_text::Metrics::new(
167                            f32::from(size) * hint_factor,
168                            f32::from(
169                                span.line_height
170                                    .unwrap_or(text.line_height)
171                                    .to_absolute(size),
172                            ) * hint_factor,
173                        ))
174                    }
175                };
176
177                let attrs = if let Some(color) = span.color {
178                    attrs.color(text::to_color(color))
179                } else {
180                    attrs
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        let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
191
192        Self(Arc::new(Internal {
193            buffer,
194            hint,
195            hint_factor,
196            font: text.font,
197            align_x: text.align_x,
198            align_y: text.align_y,
199            shaping: text.shaping,
200            wrapping: text.wrapping,
201            ellipsis: text.ellipsis,
202            bounds: text.bounds,
203            min_bounds,
204            version: font_system.version(),
205        }))
206    }
207
208    fn resize(&mut self, new_bounds: Size) {
209        let paragraph = Arc::make_mut(&mut self.0);
210
211        let mut font_system = text::font_system().write().expect("Write font system");
212
213        paragraph.buffer.set_size(
214            font_system.raw(),
215            Some(new_bounds.width * paragraph.hint_factor),
216            Some(new_bounds.height * paragraph.hint_factor),
217        );
218
219        let min_bounds = text::align(&mut paragraph.buffer, font_system.raw(), paragraph.align_x)
220            / paragraph.hint_factor;
221
222        paragraph.bounds = new_bounds;
223        paragraph.min_bounds = min_bounds;
224    }
225
226    fn compare(&self, text: Text<()>) -> core::text::Difference {
227        let font_system = text::font_system().read().expect("Read font system");
228        let paragraph = self.internal();
229        let metrics = paragraph.buffer.metrics();
230
231        if paragraph.version != font_system.version
232            || metrics.font_size != text.size.0 * paragraph.hint_factor
233            || metrics.line_height
234                != text.line_height.to_absolute(text.size).0 * paragraph.hint_factor
235            || paragraph.font != text.font
236            || paragraph.shaping != text.shaping
237            || paragraph.wrapping != text.wrapping
238            || paragraph.ellipsis != text.ellipsis
239            || paragraph.align_x != text.align_x
240            || paragraph.align_y != text.align_y
241            || paragraph.hint.then_some(paragraph.hint_factor)
242                != text::hint_factor(text.size, text.hint_factor)
243        {
244            core::text::Difference::Shape
245        } else if paragraph.bounds != text.bounds {
246            core::text::Difference::Bounds
247        } else {
248            core::text::Difference::None
249        }
250    }
251
252    fn hint_factor(&self) -> Option<f32> {
253        self.0.hint.then_some(self.0.hint_factor)
254    }
255
256    fn size(&self) -> Pixels {
257        Pixels(self.0.buffer.metrics().font_size / self.0.hint_factor)
258    }
259
260    fn font(&self) -> Font {
261        self.0.font
262    }
263
264    fn line_height(&self) -> LineHeight {
265        LineHeight::Absolute(Pixels(
266            self.0.buffer.metrics().line_height / self.0.hint_factor,
267        ))
268    }
269
270    fn align_x(&self) -> Alignment {
271        self.internal().align_x
272    }
273
274    fn align_y(&self) -> alignment::Vertical {
275        self.internal().align_y
276    }
277
278    fn wrapping(&self) -> Wrapping {
279        self.0.wrapping
280    }
281
282    fn ellipsis(&self) -> Ellipsis {
283        self.0.ellipsis
284    }
285
286    fn shaping(&self) -> Shaping {
287        self.0.shaping
288    }
289
290    fn bounds(&self) -> Size {
291        self.0.bounds
292    }
293
294    fn min_bounds(&self) -> Size {
295        self.internal().min_bounds
296    }
297
298    fn hit_test(&self, point: Point) -> Option<Hit> {
299        let cursor = self
300            .internal()
301            .buffer
302            .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
303
304        Some(Hit::CharOffset(cursor.index))
305    }
306
307    fn hit_span(&self, point: Point) -> Option<usize> {
308        let internal = self.internal();
309
310        let cursor = internal
311            .buffer
312            .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
313        let line = internal.buffer.lines.get(cursor.line)?;
314
315        if cursor.index >= line.text().len() {
316            return None;
317        }
318
319        let index = match cursor.affinity {
320            cosmic_text::Affinity::Before => cursor.index.saturating_sub(1),
321            cosmic_text::Affinity::After => cursor.index,
322        };
323
324        let mut hit = None;
325        let glyphs = line
326            .layout_opt()
327            .as_ref()?
328            .iter()
329            .flat_map(|line| line.glyphs.iter());
330
331        for glyph in glyphs {
332            if glyph.start <= index && index < glyph.end {
333                hit = Some(glyph);
334                break;
335            }
336        }
337
338        Some(hit?.metadata)
339    }
340
341    fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
342        let internal = self.internal();
343
344        let mut bounds = Vec::new();
345        let mut current_bounds = None;
346
347        let glyphs = internal
348            .buffer
349            .layout_runs()
350            .flat_map(|run| {
351                let line_top = run.line_top;
352                let line_height = run.line_height;
353
354                run.glyphs
355                    .iter()
356                    .map(move |glyph| (line_top, line_height, glyph))
357            })
358            .skip_while(|(_, _, glyph)| glyph.metadata != index)
359            .take_while(|(_, _, glyph)| glyph.metadata == index);
360
361        for (line_top, line_height, glyph) in glyphs {
362            let y = line_top + glyph.y;
363
364            let new_bounds = || {
365                Rectangle::new(
366                    Point::new(glyph.x, y),
367                    Size::new(glyph.w, glyph.line_height_opt.unwrap_or(line_height)),
368                ) * (1.0 / self.0.hint_factor)
369            };
370
371            match current_bounds.as_mut() {
372                None => {
373                    current_bounds = Some(new_bounds());
374                }
375                Some(current_bounds) if y != current_bounds.y => {
376                    bounds.push(*current_bounds);
377                    *current_bounds = new_bounds();
378                }
379                Some(current_bounds) => {
380                    current_bounds.width += glyph.w / self.0.hint_factor;
381                }
382            }
383        }
384
385        bounds.extend(current_bounds);
386        bounds
387    }
388
389    fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
390        use unicode_segmentation::UnicodeSegmentation;
391
392        let run = self.internal().buffer.layout_runs().nth(line)?;
393
394        // index represents a grapheme, not a glyph
395        // Let's find the first glyph for the given grapheme cluster
396        let mut last_start = None;
397        let mut last_grapheme_count = 0;
398        let mut graphemes_seen = 0;
399
400        let glyph = run
401            .glyphs
402            .iter()
403            .find(|glyph| {
404                if Some(glyph.start) != last_start {
405                    last_grapheme_count = run.text[glyph.start..glyph.end].graphemes(false).count();
406                    last_start = Some(glyph.start);
407                    graphemes_seen += last_grapheme_count;
408                }
409
410                graphemes_seen >= index
411            })
412            .or_else(|| run.glyphs.last())?;
413
414        let advance = if index == 0 {
415            0.0
416        } else {
417            glyph.w
418                * (1.0
419                    - graphemes_seen.saturating_sub(index) as f32
420                        / last_grapheme_count.max(1) as f32)
421        };
422
423        Some(Point::new(
424            (glyph.x + glyph.x_offset * glyph.font_size + advance) / self.0.hint_factor,
425            (glyph.y - glyph.y_offset * glyph.font_size) / self.0.hint_factor,
426        ))
427    }
428}
429
430impl Default for Paragraph {
431    fn default() -> Self {
432        Self(Arc::new(Internal::default()))
433    }
434}
435
436impl fmt::Debug for Paragraph {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        let paragraph = self.internal();
439
440        f.debug_struct("Paragraph")
441            .field("font", &paragraph.font)
442            .field("shaping", &paragraph.shaping)
443            .field("horizontal_alignment", &paragraph.align_x)
444            .field("vertical_alignment", &paragraph.align_y)
445            .field("bounds", &paragraph.bounds)
446            .field("min_bounds", &paragraph.min_bounds)
447            .finish()
448    }
449}
450
451impl PartialEq for Internal {
452    fn eq(&self, other: &Self) -> bool {
453        self.font == other.font
454            && self.shaping == other.shaping
455            && self.align_x == other.align_x
456            && self.align_y == other.align_y
457            && self.bounds == other.bounds
458            && self.min_bounds == other.min_bounds
459            && self.buffer.metrics() == other.buffer.metrics()
460    }
461}
462
463impl Default for Internal {
464    fn default() -> Self {
465        Self {
466            buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
467                font_size: 1.0,
468                line_height: 1.0,
469            }),
470            font: Font::default(),
471            shaping: Shaping::default(),
472            wrapping: Wrapping::default(),
473            ellipsis: Ellipsis::default(),
474            align_x: Alignment::Default,
475            align_y: alignment::Vertical::Top,
476            bounds: Size::ZERO,
477            min_bounds: Size::ZERO,
478            version: text::Version::default(),
479            hint: false,
480            hint_factor: 1.0,
481        }
482    }
483}
484
485/// A weak reference to a [`Paragraph`].
486#[derive(Debug, Clone)]
487pub struct Weak {
488    raw: sync::Weak<Internal>,
489    /// The minimum bounds of the [`Paragraph`].
490    pub min_bounds: Size,
491    /// The horizontal alignment of the [`Paragraph`].
492    pub align_x: Alignment,
493    /// The vertical alignment of the [`Paragraph`].
494    pub align_y: alignment::Vertical,
495}
496
497impl Weak {
498    /// Tries to update the reference into a [`Paragraph`].
499    pub fn upgrade(&self) -> Option<Paragraph> {
500        self.raw.upgrade().map(Paragraph)
501    }
502}
503
504impl PartialEq for Weak {
505    fn eq(&self, other: &Self) -> bool {
506        match (self.raw.upgrade(), other.raw.upgrade()) {
507            (Some(p1), Some(p2)) => p1 == p2,
508            _ => false,
509        }
510    }
511}