1use crate::core;
3use crate::core::alignment;
4use crate::core::text::{
5 Alignment, Ellipsis, Hit, LineHeight, Shaping, Span, Target, 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#[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 ellipsis: Ellipsis,
24 align_x: Alignment,
25 align_y: alignment::Vertical,
26 bounds: Size,
27 min_bounds: Size,
28 version: text::Version,
29 hint: bool,
30 hint_factor: f32,
31 selection: Option<Selection>,
32}
33
34#[derive(Clone)]
35struct Selection {
36 start: cosmic_text::Cursor,
37 end: cosmic_text::Cursor,
38 regions: Vec<Rectangle>,
39}
40
41impl Paragraph {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn buffer(&self) -> &cosmic_text::Buffer {
49 &self.internal().buffer
50 }
51
52 pub fn downgrade(&self) -> Weak {
58 let paragraph = self.internal();
59
60 Weak {
61 raw: Arc::downgrade(paragraph),
62 min_bounds: paragraph.min_bounds,
63 align_x: paragraph.align_x,
64 align_y: paragraph.align_y,
65 }
66 }
67
68 fn internal(&self) -> &Arc<Internal> {
69 &self.0
70 }
71}
72
73impl core::text::Paragraph for Paragraph {
74 fn with_text(text: Text<&str>) -> Self {
75 log::trace!("Allocating plain paragraph: {}", text.content);
76
77 let mut font_system = text::font_system().write().expect("Write font system");
78
79 let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
80 Some(hint_factor) => (true, hint_factor),
81 _ => (false, 1.0),
82 };
83
84 let mut buffer = cosmic_text::Buffer::new(
85 font_system.raw(),
86 cosmic_text::Metrics::new(
87 f32::from(text.size) * hint_factor,
88 f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
89 ),
90 );
91
92 if hint {
93 buffer.set_hinting(cosmic_text::Hinting::Enabled);
94 }
95
96 buffer.set_size(
97 Some(text.bounds.width * hint_factor),
98 Some(text.bounds.height * hint_factor),
99 );
100
101 buffer.set_wrap(text::to_wrap(text.wrapping));
102 buffer.set_ellipsize(text::to_ellipsize(
103 text.ellipsis,
104 text.bounds.height * hint_factor,
105 ));
106
107 buffer.set_text(
108 text.content,
109 &text::to_attributes(text.font),
110 text::to_shaping(text.shaping, text.content),
111 None,
112 );
113 buffer.shape_until_scroll(font_system.raw(), false);
114
115 let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
116
117 Self(Arc::new(Internal {
118 buffer,
119 hint,
120 hint_factor,
121 font: text.font,
122 align_x: text.align_x,
123 align_y: text.align_y,
124 shaping: text.shaping,
125 wrapping: text.wrapping,
126 ellipsis: text.ellipsis,
127 bounds: text.bounds,
128 min_bounds,
129 version: font_system.version(),
130 selection: None,
131 }))
132 }
133
134 fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
135 log::trace!("Allocating rich paragraph: {} spans", text.content.len());
136
137 let mut font_system = text::font_system().write().expect("Write font system");
138
139 let (hint, hint_factor) = match text::hint_factor(text.size, text.hint_factor) {
140 Some(hint_factor) => (true, hint_factor),
141 _ => (false, 1.0),
142 };
143
144 let mut buffer = cosmic_text::Buffer::new(
145 font_system.raw(),
146 cosmic_text::Metrics::new(
147 f32::from(text.size) * hint_factor,
148 f32::from(text.line_height.to_absolute(text.size)) * hint_factor,
149 ),
150 );
151
152 if hint {
153 buffer.set_hinting(cosmic_text::Hinting::Enabled);
154 }
155
156 buffer.set_size(
157 Some(text.bounds.width * hint_factor),
158 Some(text.bounds.height * hint_factor),
159 );
160
161 buffer.set_wrap(text::to_wrap(text.wrapping));
162
163 buffer.set_rich_text(
164 text.content.iter().enumerate().map(|(i, span)| {
165 let attrs = text::to_attributes(span.font.unwrap_or(text.font));
166
167 let attrs = match (span.size, span.line_height) {
168 (None, None) => attrs,
169 _ => {
170 let size = span.size.unwrap_or(text.size);
171
172 attrs.metrics(cosmic_text::Metrics::new(
173 f32::from(size) * hint_factor,
174 f32::from(
175 span.line_height
176 .unwrap_or(text.line_height)
177 .to_absolute(size),
178 ) * hint_factor,
179 ))
180 }
181 };
182
183 let attrs = if let Some(color) = span.color {
184 attrs.color(text::to_color(color))
185 } else {
186 attrs
187 };
188
189 let attrs = attrs.padding(cosmic_text::SpanPadding {
190 start: span.padding.left,
191 end: span.padding.right,
192 });
193
194 (span.text.as_ref(), attrs.metadata(i))
195 }),
196 &text::to_attributes(text.font),
197 cosmic_text::Shaping::Advanced,
198 None,
199 );
200
201 buffer.shape_until_scroll(font_system.raw(), false);
202
203 let min_bounds = text::align(&mut buffer, font_system.raw(), text.align_x) / hint_factor;
204
205 Self(Arc::new(Internal {
206 buffer,
207 hint,
208 hint_factor,
209 font: text.font,
210 align_x: text.align_x,
211 align_y: text.align_y,
212 shaping: text.shaping,
213 wrapping: text.wrapping,
214 ellipsis: text.ellipsis,
215 bounds: text.bounds,
216 min_bounds,
217 version: font_system.version(),
218 selection: None,
219 }))
220 }
221
222 fn resize(&mut self, new_bounds: Size) {
223 let paragraph = Arc::make_mut(&mut self.0);
224
225 let mut font_system = text::font_system().write().expect("Write font system");
226
227 paragraph.buffer.set_size(
228 Some(new_bounds.width * paragraph.hint_factor),
229 Some(new_bounds.height * paragraph.hint_factor),
230 );
231 paragraph
232 .buffer
233 .shape_until_scroll(font_system.raw(), false);
234
235 let min_bounds = text::align(&mut paragraph.buffer, font_system.raw(), paragraph.align_x)
236 / paragraph.hint_factor;
237
238 paragraph.bounds = new_bounds;
239 paragraph.min_bounds = min_bounds;
240
241 if let Some(Selection { start, end, .. }) = paragraph.selection {
242 paragraph.selection = Some(Selection {
243 start,
244 end,
245 regions: text::regions(¶graph.buffer, start, end),
246 });
247 }
248 }
249
250 fn compare(&self, text: Text<()>) -> core::text::Difference {
251 let font_system = text::font_system().read().expect("Read font system");
252 let paragraph = self.internal();
253 let metrics = paragraph.buffer.metrics();
254
255 if paragraph.version != font_system.version
256 || metrics.font_size != text.size.0 * paragraph.hint_factor
257 || metrics.line_height
258 != text.line_height.to_absolute(text.size).0 * paragraph.hint_factor
259 || paragraph.font != text.font
260 || paragraph.shaping != text.shaping
261 || paragraph.wrapping != text.wrapping
262 || paragraph.ellipsis != text.ellipsis
263 || paragraph.align_x != text.align_x
264 || paragraph.align_y != text.align_y
265 || paragraph.hint.then_some(paragraph.hint_factor)
266 != text::hint_factor(text.size, text.hint_factor)
267 {
268 core::text::Difference::Shape
269 } else if paragraph.bounds != text.bounds {
270 core::text::Difference::Bounds
271 } else {
272 core::text::Difference::None
273 }
274 }
275
276 fn hint_factor(&self) -> Option<f32> {
277 self.0.hint.then_some(self.0.hint_factor)
278 }
279
280 fn size(&self) -> Pixels {
281 Pixels(self.0.buffer.metrics().font_size / self.0.hint_factor)
282 }
283
284 fn font(&self) -> Font {
285 self.0.font
286 }
287
288 fn line_height(&self) -> LineHeight {
289 LineHeight::Absolute(Pixels(
290 self.0.buffer.metrics().line_height / self.0.hint_factor,
291 ))
292 }
293
294 fn align_x(&self) -> Alignment {
295 self.internal().align_x
296 }
297
298 fn align_y(&self) -> alignment::Vertical {
299 self.internal().align_y
300 }
301
302 fn wrapping(&self) -> Wrapping {
303 self.0.wrapping
304 }
305
306 fn ellipsis(&self) -> Ellipsis {
307 self.0.ellipsis
308 }
309
310 fn shaping(&self) -> Shaping {
311 self.0.shaping
312 }
313
314 fn bounds(&self) -> Size {
315 self.0.bounds
316 }
317
318 fn min_bounds(&self) -> Size {
319 self.internal().min_bounds
320 }
321
322 fn hit_test(&self, point: Point) -> Option<Hit> {
323 let cursor = self
324 .internal()
325 .buffer
326 .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
327
328 Some(Hit {
329 line: cursor.line,
330 index: cursor.index,
331 })
332 }
333
334 fn hit_glyph(&self, point: Point) -> bool {
335 self.internal().buffer.layout_runs().any(|run| {
336 let line_top = run.line_top;
337 let line_height = run.line_height;
338
339 if line_top > point.y || line_top + line_height < point.y {
340 return false;
341 }
342
343 run.glyphs
344 .iter()
345 .any(|glyph| glyph.x <= point.x && glyph.x + glyph.w >= point.x)
346 })
347 }
348
349 fn hit_span(&self, point: Point) -> Option<usize> {
350 let internal = self.internal();
351
352 let cursor = internal
353 .buffer
354 .hit(point.x * self.0.hint_factor, point.y * self.0.hint_factor)?;
355 let line = internal.buffer.lines.get(cursor.line)?;
356
357 if cursor.index >= line.text().len() {
358 return None;
359 }
360
361 let index = match cursor.affinity {
362 cosmic_text::Affinity::Before => cursor.index.saturating_sub(1),
363 cosmic_text::Affinity::After => cursor.index,
364 };
365
366 let mut hit = None;
367 let glyphs = line
368 .layout_opt()
369 .as_ref()?
370 .iter()
371 .flat_map(|line| line.glyphs.iter());
372
373 for glyph in glyphs {
374 if glyph.start <= index && index < glyph.end {
375 hit = Some(glyph);
376 break;
377 }
378 }
379
380 Some(hit?.metadata)
381 }
382
383 fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
384 let internal = self.internal();
385
386 let scale = 1.0 / internal.hint_factor;
387
388 let mut bounds = Vec::new();
389 let mut current = None;
390 let mut current_baseline = 0.0;
391
392 let mut y = 0.0;
393 let buffer_height = internal.buffer.metrics().line_height;
394 let glyphs = internal
395 .buffer
396 .lines
397 .iter()
398 .filter_map(|paragraph| paragraph.layout_opt().map(Vec::as_slice))
399 .flat_map(|lines| lines.iter())
400 .flat_map(move |line| {
401 let line_height = line.line_height(buffer_height);
402 let ink_height = line.max_ascent + line.max_descent;
403
404 let baseline = y + (line_height - ink_height) / 2.0 + line.max_ascent;
408
409 let glyphs = line.glyphs.iter().map(move |glyph| (baseline, glyph));
410
411 y += line_height;
412
413 glyphs
414 })
415 .skip_while(|(_, glyph)| glyph.metadata != index)
416 .take_while(|(_, glyph)| glyph.metadata == index);
417
418 for (baseline, glyph) in glyphs {
419 let anchor = baseline + glyph.y - glyph.font_size * glyph.y_offset;
422 let ink_top = anchor - glyph.ascender;
423 let ink_bottom = anchor + glyph.descender;
424
425 match current.as_mut() {
426 None => {
427 current_baseline = baseline;
428 current = Some(
429 Rectangle::new(
430 Point::new(glyph.x, ink_top),
431 Size::new(glyph.w, ink_bottom - ink_top),
432 ) * scale,
433 );
434 }
435 Some(current) if baseline != current_baseline => {
436 bounds.push(*current);
437 current_baseline = baseline;
438 *current = Rectangle::new(
439 Point::new(glyph.x, ink_top),
440 Size::new(glyph.w, ink_bottom - ink_top),
441 ) * scale;
442 }
443 Some(current) => {
444 let left = current.x.min(glyph.x * scale);
447 let top = current.y.min(ink_top * scale);
448 let right = (current.x + current.width).max((glyph.x + glyph.w) * scale);
449 let bottom = (current.y + current.height).max(ink_bottom * scale);
450 *current = Rectangle::new(
451 Point::new(left, top),
452 Size::new(right - left, bottom - top),
453 );
454 }
455 }
456 }
457
458 bounds.extend(current);
459 bounds
460 }
461
462 fn select(&mut self, start: Point, end: Point, target: Target) {
463 use cosmic_text::Edit;
464
465 let paragraph = Arc::make_mut(&mut self.0);
466 let mut font_system = text::font_system().write().expect("write font system");
467
468 let mut editor =
470 cosmic_text::Editor::new(cosmic_text::BufferRef::Borrowed(&mut paragraph.buffer));
471
472 editor.action(&mut font_system.raw, {
473 let x = start.x as i32;
474 let y = start.y as i32;
475
476 match target {
477 Target::Character => cosmic_text::Action::Click { x, y },
478 Target::Word => cosmic_text::Action::DoubleClick { x, y },
479 Target::Line => cosmic_text::Action::TripleClick { x, y },
480 }
481 });
482
483 editor.action(
484 &mut font_system.raw,
485 cosmic_text::Action::Drag {
486 x: end.x as i32,
487 y: end.y as i32,
488 },
489 );
490
491 let Some((start, end)) = editor.selection_bounds() else {
492 paragraph.selection = None;
493 return;
494 };
495
496 let cursor = editor.cursor();
497
498 let selection = match editor.selection() {
499 cosmic_text::Selection::None => cursor,
500 cosmic_text::Selection::Normal(selection)
501 | cosmic_text::Selection::Line(selection)
502 | cosmic_text::Selection::Word(selection) => selection,
503 };
504
505 if cursor == selection
508 && ((cursor.line == 0 && cursor.index == 0)
509 || (cursor.line + 1 == paragraph.buffer.lines.len()
510 && cursor.index == paragraph.buffer.lines[cursor.line].text().len()))
511 {
512 paragraph.selection = None;
513 return;
514 }
515
516 paragraph.selection = Some(Selection {
517 start,
518 end,
519 regions: text::regions(¶graph.buffer, start, end),
520 });
521 }
522
523 fn select_all(&mut self) {
524 let paragraph = Arc::make_mut(&mut self.0);
525
526 let start = cosmic_text::Cursor {
527 line: 0,
528 index: 0,
529 affinity: cosmic_text::Affinity::Before,
530 };
531
532 let end = cosmic_text::Cursor {
533 line: paragraph.buffer.lines.len().saturating_sub(1),
534 index: paragraph
535 .buffer
536 .lines
537 .last()
538 .map(|line| line.text().floor_char_boundary(line.text().len()))
539 .unwrap_or_default(),
540 affinity: cosmic_text::Affinity::After,
541 };
542
543 paragraph.selection = Some(Selection {
544 start,
545 end,
546 regions: text::regions(¶graph.buffer, start, end),
547 });
548 }
549
550 fn deselect(&mut self) {
551 let paragraph = Arc::make_mut(&mut self.0);
552 paragraph.selection = None;
553 }
554
555 fn selection(&self) -> &[Rectangle] {
556 self.internal()
557 .selection
558 .as_ref()
559 .map(|selection| selection.regions.as_slice())
560 .unwrap_or_default()
561 }
562
563 fn copy(&mut self) -> Option<String> {
564 use cosmic_text::Edit;
565
566 let paragraph = Arc::make_mut(&mut self.0);
567 let selection = paragraph.selection.as_ref()?;
568
569 let mut editor =
570 cosmic_text::Editor::new(cosmic_text::BufferRef::Borrowed(&mut paragraph.buffer));
571
572 editor.set_selection(cosmic_text::Selection::Normal(selection.start));
573 editor.set_cursor(selection.end);
574 editor.copy_selection()
575 }
576}
577
578impl Default for Paragraph {
579 fn default() -> Self {
580 Self(Arc::new(Internal::default()))
581 }
582}
583
584impl fmt::Debug for Paragraph {
585 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586 let paragraph = self.internal();
587
588 f.debug_struct("Paragraph")
589 .field("font", ¶graph.font)
590 .field("shaping", ¶graph.shaping)
591 .field("horizontal_alignment", ¶graph.align_x)
592 .field("vertical_alignment", ¶graph.align_y)
593 .field("bounds", ¶graph.bounds)
594 .field("min_bounds", ¶graph.min_bounds)
595 .finish()
596 }
597}
598
599impl PartialEq for Internal {
600 fn eq(&self, other: &Self) -> bool {
601 self.font == other.font
602 && self.shaping == other.shaping
603 && self.align_x == other.align_x
604 && self.align_y == other.align_y
605 && self.bounds == other.bounds
606 && self.min_bounds == other.min_bounds
607 && self.buffer.metrics() == other.buffer.metrics()
608 }
609}
610
611impl Default for Internal {
612 fn default() -> Self {
613 Self {
614 buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
615 font_size: 1.0,
616 line_height: 1.0,
617 }),
618 font: Font::default(),
619 shaping: Shaping::default(),
620 wrapping: Wrapping::default(),
621 ellipsis: Ellipsis::default(),
622 align_x: Alignment::Default,
623 align_y: alignment::Vertical::Top,
624 bounds: Size::ZERO,
625 min_bounds: Size::ZERO,
626 version: text::Version::default(),
627 hint: false,
628 hint_factor: 1.0,
629 selection: None,
630 }
631 }
632}
633
634#[derive(Debug, Clone)]
636pub struct Weak {
637 raw: sync::Weak<Internal>,
638 pub min_bounds: Size,
640 pub align_x: Alignment,
642 pub align_y: alignment::Vertical,
644}
645
646impl Weak {
647 pub fn upgrade(&self) -> Option<Paragraph> {
649 self.raw.upgrade().map(Paragraph)
650 }
651}
652
653impl PartialEq for Weak {
654 fn eq(&self, other: &Self) -> bool {
655 match (self.raw.upgrade(), other.raw.upgrade()) {
656 (Some(p1), Some(p2)) => p1 == p2,
657 _ => false,
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::core::text::Paragraph as _;
666
667 fn rich_paragraph(content: &[Span<'_, ()>]) -> Paragraph {
668 let text = Text {
669 content,
670 bounds: Size::new(1000.0, f32::INFINITY),
671 size: Pixels(20.0),
672 line_height: LineHeight::Relative(1.5),
673 font: Font::default(),
674 align_x: Alignment::Default,
675 align_y: alignment::Vertical::Top,
676 shaping: Shaping::default(),
677 wrapping: Wrapping::default(),
678 ellipsis: Ellipsis::default(),
679 hint_factor: None,
680 };
681
682 Paragraph::with_spans(text)
683 }
684
685 #[test]
688 fn span_bounds_cover_ink() {
689 let paragraph = rich_paragraph(&[Span::new("a"), Span::new("a").size(40.0)]);
690
691 let small = paragraph.span_bounds(0);
692 let big = paragraph.span_bounds(1);
693
694 assert_eq!(small.len(), 1);
695 assert_eq!(big.len(), 1);
696
697 let buffer = paragraph.buffer();
698 let line = &buffer.lines[0].layout_opt().unwrap()[0];
699 let line_height = line.line_height(buffer.metrics().line_height);
700 let ink_height = line.max_ascent + line.max_descent;
701 let centering = (line_height - ink_height) / 2.0;
702
703 assert!((big[0].y - centering).abs() < 1e-2);
705 assert!((big[0].height - ink_height).abs() < 1e-2);
706
707 let baseline = centering + line.max_ascent;
709 assert!((small[0].y + line.max_ascent / 2.0 - baseline).abs() < 1e-2);
710 assert!((big[0].y + line.max_ascent - baseline).abs() < 1e-2);
711
712 assert!(small[0].y > centering);
714 assert!(small[0].y + small[0].height < centering + ink_height);
715 assert!((small[0].height - ink_height / 2.0).abs() < 1e-2);
716
717 if ink_height < line_height {
719 assert!(big[0].height < line_height);
720 }
721 }
722
723 #[test]
725 fn span_bounds_cover_width() {
726 let paragraph = rich_paragraph(&[Span::new("Hello"), Span::new(", world!")]);
727
728 let first = paragraph.span_bounds(0);
729 let second = paragraph.span_bounds(1);
730
731 assert_eq!(first.len(), 1);
732 assert_eq!(second.len(), 1);
733
734 let buffer = paragraph.buffer();
735 let line = &buffer.lines[0].layout_opt().unwrap()[0];
736
737 assert!((first[0].x - 0.0).abs() < 1e-2);
740 assert!(first[0].width > 0.0);
741 assert!(first[0].width < line.w);
742
743 assert!((second[0].x - (first[0].x + first[0].width)).abs() < 1e-2);
746 assert!((second[0].x + second[0].width - line.w).abs() < 1e-2);
747 }
748
749 #[test]
751 fn span_bounds_cover_each_line() {
752 let paragraph = rich_paragraph(&[Span::new("ab\ncd")]);
753
754 let bounds = paragraph.span_bounds(0);
755
756 assert_eq!(bounds.len(), 2);
757
758 let buffer = paragraph.buffer();
759 let line_height = buffer.metrics().line_height;
760
761 for bounds in &bounds {
762 assert!((bounds.x - 0.0).abs() < 1e-2);
763 assert!(bounds.height > 0.0);
764 assert!(bounds.height < line_height);
765 }
766
767 assert!(bounds[0].y + bounds[0].height <= bounds[1].y + 1e-2);
770 assert!((bounds[0].height - bounds[1].height).abs() < 1e-2);
771 }
772}