1use 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#[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 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn buffer(&self) -> &cosmic_text::Buffer {
39 &self.internal().buffer
40 }
41
42 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 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 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 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", ¶graph.font)
437 .field("shaping", ¶graph.shaping)
438 .field("horizontal_alignment", ¶graph.align_x)
439 .field("vertical_alignment", ¶graph.align_y)
440 .field("bounds", ¶graph.bounds)
441 .field("min_bounds", ¶graph.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#[derive(Debug, Clone)]
482pub struct Weak {
483 raw: sync::Weak<Internal>,
484 pub min_bounds: Size,
486 pub align_x: Alignment,
488 pub align_y: alignment::Vertical,
490}
491
492impl Weak {
493 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 #[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 assert!((big[0].y - centering).abs() < 1e-2);
551 assert!((big[0].height - ink_height).abs() < 1e-2);
552
553 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 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 if ink_height < line_height {
565 assert!(big[0].height < line_height);
566 }
567 }
568
569 #[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 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 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 #[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 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}