Skip to main content

iced_graphics/text/
paragraph.rs

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