diff options
Diffstat (limited to 'graphics/src/text/paragraph.rs')
-rw-r--r-- | graphics/src/text/paragraph.rs | 240 |
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(¶graph.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", ¶graph.content) .field("font", ¶graph.font) .field("shaping", ¶graph.shaping) .field("horizontal_alignment", ¶graph.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) } } |