summaryrefslogtreecommitdiffstats
path: root/graphics/src/text/paragraph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'graphics/src/text/paragraph.rs')
-rw-r--r--graphics/src/text/paragraph.rs240
1 files changed, 179 insertions, 61 deletions
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index 31a323ac..07ddbb82 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -1,8 +1,8 @@
//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
-use crate::core::text::{Hit, LineHeight, Shaping, Text};
-use crate::core::{Font, Pixels, Point, Size};
+use crate::core::text::{Hit, Shaping, Span, Text, Wrapping};
+use crate::core::{Font, Point, Rectangle, Size};
use crate::text;
use std::fmt;
@@ -10,13 +10,14 @@ use std::sync::{self, Arc};
/// A bunch of text.
#[derive(Clone, PartialEq)]
-pub struct Paragraph(Option<Arc<Internal>>);
+pub struct Paragraph(Arc<Internal>);
+#[derive(Clone)]
struct Internal {
buffer: cosmic_text::Buffer,
- content: String, // TODO: Reuse from `buffer` (?)
font: Font,
shaping: Shaping,
+ wrapping: Wrapping,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
bounds: Size,
@@ -52,9 +53,7 @@ impl Paragraph {
}
fn internal(&self) -> &Arc<Internal> {
- self.0
- .as_ref()
- .expect("paragraph should always be initialized")
+ &self.0
}
}
@@ -62,7 +61,7 @@ impl core::text::Paragraph for Paragraph {
type Font = Font;
fn with_text(text: Text<&str>) -> Self {
- log::trace!("Allocating paragraph: {}", text.content);
+ log::trace!("Allocating plain paragraph: {}", text.content);
let mut font_system =
text::font_system().write().expect("Write font system");
@@ -77,8 +76,8 @@ impl core::text::Paragraph for Paragraph {
buffer.set_size(
font_system.raw(),
- text.bounds.width,
- text.bounds.height,
+ Some(text.bounds.width),
+ Some(text.bounds.height),
);
buffer.set_text(
@@ -90,73 +89,113 @@ impl core::text::Paragraph for Paragraph {
let min_bounds = text::measure(&buffer);
- Self(Some(Arc::new(Internal {
+ Self(Arc::new(Internal {
buffer,
- content: text.content.to_owned(),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ wrapping: text.wrapping,
bounds: text.bounds,
min_bounds,
version: font_system.version(),
- })))
+ }))
+ }
+
+ fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
+ log::trace!("Allocating rich paragraph: {} spans", text.content.len());
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let mut buffer = cosmic_text::Buffer::new(
+ font_system.raw(),
+ cosmic_text::Metrics::new(
+ text.size.into(),
+ text.line_height.to_absolute(text.size).into(),
+ ),
+ );
+
+ buffer.set_size(
+ font_system.raw(),
+ Some(text.bounds.width),
+ Some(text.bounds.height),
+ );
+
+ buffer.set_rich_text(
+ font_system.raw(),
+ text.content.iter().enumerate().map(|(i, span)| {
+ let attrs = text::to_attributes(span.font.unwrap_or(text.font));
+
+ let attrs = match (span.size, span.line_height) {
+ (None, None) => attrs,
+ _ => {
+ let size = span.size.unwrap_or(text.size);
+
+ attrs.metrics(cosmic_text::Metrics::new(
+ size.into(),
+ span.line_height
+ .unwrap_or(text.line_height)
+ .to_absolute(size)
+ .into(),
+ ))
+ }
+ };
+
+ let attrs = if let Some(color) = span.color {
+ attrs.color(text::to_color(color))
+ } else {
+ attrs
+ };
+
+ (span.text.as_ref(), attrs.metadata(i))
+ }),
+ text::to_attributes(text.font),
+ text::to_shaping(text.shaping),
+ );
+
+ let min_bounds = text::measure(&buffer);
+
+ Self(Arc::new(Internal {
+ buffer,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ wrapping: text.wrapping,
+ bounds: text.bounds,
+ min_bounds,
+ version: font_system.version(),
+ }))
}
fn resize(&mut self, new_bounds: Size) {
- let paragraph = self
- .0
- .take()
- .expect("paragraph should always be initialized");
-
- match Arc::try_unwrap(paragraph) {
- Ok(mut internal) => {
- let mut font_system =
- text::font_system().write().expect("Write font system");
-
- internal.buffer.set_size(
- font_system.raw(),
- new_bounds.width,
- new_bounds.height,
- );
-
- internal.bounds = new_bounds;
- internal.min_bounds = text::measure(&internal.buffer);
-
- self.0 = Some(Arc::new(internal));
- }
- Err(internal) => {
- let metrics = internal.buffer.metrics();
-
- // If there is a strong reference somewhere, we recompute the
- // buffer from scratch
- *self = Self::with_text(Text {
- content: &internal.content,
- bounds: internal.bounds,
- size: Pixels(metrics.font_size),
- line_height: LineHeight::Absolute(Pixels(
- metrics.line_height,
- )),
- font: internal.font,
- horizontal_alignment: internal.horizontal_alignment,
- vertical_alignment: internal.vertical_alignment,
- shaping: internal.shaping,
- });
- }
- }
+ let paragraph = Arc::make_mut(&mut self.0);
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ paragraph.buffer.set_size(
+ font_system.raw(),
+ Some(new_bounds.width),
+ Some(new_bounds.height),
+ );
+
+ paragraph.bounds = new_bounds;
+ paragraph.min_bounds = text::measure(&paragraph.buffer);
}
- fn compare(&self, text: Text<&str>) -> core::text::Difference {
+ fn compare(&self, text: Text<()>) -> core::text::Difference {
let font_system = text::font_system().read().expect("Read font system");
let paragraph = self.internal();
let metrics = paragraph.buffer.metrics();
if paragraph.version != font_system.version
- || paragraph.content != text.content
|| metrics.font_size != text.size.0
|| metrics.line_height != text.line_height.to_absolute(text.size).0
|| paragraph.font != text.font
|| paragraph.shaping != text.shaping
+ || paragraph.wrapping != text.wrapping
|| paragraph.horizontal_alignment != text.horizontal_alignment
|| paragraph.vertical_alignment != text.vertical_alignment
{
@@ -186,6 +225,87 @@ impl core::text::Paragraph for Paragraph {
Some(Hit::CharOffset(cursor.index))
}
+ fn hit_span(&self, point: Point) -> Option<usize> {
+ let internal = self.internal();
+
+ let cursor = internal.buffer.hit(point.x, point.y)?;
+ let line = internal.buffer.lines.get(cursor.line)?;
+
+ let mut last_glyph = None;
+ let mut glyphs = line
+ .layout_opt()
+ .as_ref()?
+ .iter()
+ .flat_map(|line| line.glyphs.iter())
+ .peekable();
+
+ while let Some(glyph) = glyphs.peek() {
+ if glyph.start <= cursor.index && cursor.index < glyph.end {
+ break;
+ }
+
+ last_glyph = glyphs.next();
+ }
+
+ let glyph = match cursor.affinity {
+ cosmic_text::Affinity::Before => last_glyph,
+ cosmic_text::Affinity::After => glyphs.next(),
+ }?;
+
+ Some(glyph.metadata)
+ }
+
+ fn span_bounds(&self, index: usize) -> Vec<Rectangle> {
+ let internal = self.internal();
+
+ let mut bounds = Vec::new();
+ let mut current_bounds = None;
+
+ let glyphs = internal
+ .buffer
+ .layout_runs()
+ .flat_map(|run| {
+ let line_top = run.line_top;
+ let line_height = run.line_height;
+
+ run.glyphs
+ .iter()
+ .map(move |glyph| (line_top, line_height, glyph))
+ })
+ .skip_while(|(_, _, glyph)| glyph.metadata != index)
+ .take_while(|(_, _, glyph)| glyph.metadata == index);
+
+ for (line_top, line_height, glyph) in glyphs {
+ let y = line_top + glyph.y;
+
+ let new_bounds = || {
+ Rectangle::new(
+ Point::new(glyph.x, y),
+ Size::new(
+ glyph.w,
+ glyph.line_height_opt.unwrap_or(line_height),
+ ),
+ )
+ };
+
+ match current_bounds.as_mut() {
+ None => {
+ current_bounds = Some(new_bounds());
+ }
+ Some(current_bounds) if y != current_bounds.y => {
+ bounds.push(*current_bounds);
+ *current_bounds = new_bounds();
+ }
+ Some(current_bounds) => {
+ current_bounds.width += glyph.w;
+ }
+ }
+ }
+
+ bounds.extend(current_bounds);
+ bounds
+ }
+
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
use unicode_segmentation::UnicodeSegmentation;
@@ -231,7 +351,7 @@ impl core::text::Paragraph for Paragraph {
impl Default for Paragraph {
fn default() -> Self {
- Self(Some(Arc::new(Internal::default())))
+ Self(Arc::new(Internal::default()))
}
}
@@ -240,7 +360,6 @@ impl fmt::Debug for Paragraph {
let paragraph = self.internal();
f.debug_struct("Paragraph")
- .field("content", &paragraph.content)
.field("font", &paragraph.font)
.field("shaping", &paragraph.shaping)
.field("horizontal_alignment", &paragraph.horizontal_alignment)
@@ -253,8 +372,7 @@ impl fmt::Debug for Paragraph {
impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
- self.content == other.content
- && self.font == other.font
+ self.font == other.font
&& self.shaping == other.shaping
&& self.horizontal_alignment == other.horizontal_alignment
&& self.vertical_alignment == other.vertical_alignment
@@ -271,9 +389,9 @@ impl Default for Internal {
font_size: 1.0,
line_height: 1.0,
}),
- content: String::new(),
font: Font::default(),
shaping: Shaping::default(),
+ wrapping: Wrapping::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
bounds: Size::ZERO,
@@ -298,7 +416,7 @@ pub struct Weak {
impl Weak {
/// Tries to update the reference into a [`Paragraph`].
pub fn upgrade(&self) -> Option<Paragraph> {
- self.raw.upgrade().map(Some).map(Paragraph)
+ self.raw.upgrade().map(Paragraph)
}
}