iced_graphics/text/
paragraph.rs

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