iced_graphics/text/
paragraph.rs

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