diff options
Diffstat (limited to 'graphics')
-rw-r--r-- | graphics/Cargo.toml | 1 | ||||
-rw-r--r-- | graphics/src/geometry.rs | 1 | ||||
-rw-r--r-- | graphics/src/geometry/fill.rs | 2 | ||||
-rw-r--r-- | graphics/src/geometry/frame.rs | 55 | ||||
-rw-r--r-- | graphics/src/geometry/stroke.rs | 2 | ||||
-rw-r--r-- | graphics/src/geometry/style.rs | 2 | ||||
-rw-r--r-- | graphics/src/geometry/text.rs | 5 | ||||
-rw-r--r-- | graphics/src/gradient.rs | 2 | ||||
-rw-r--r-- | graphics/src/image.rs | 45 | ||||
-rw-r--r-- | graphics/src/text.rs | 25 | ||||
-rw-r--r-- | graphics/src/text/cache.rs | 4 | ||||
-rw-r--r-- | graphics/src/text/editor.rs | 304 | ||||
-rw-r--r-- | graphics/src/text/paragraph.rs | 240 |
13 files changed, 414 insertions, 274 deletions
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index e8d27d07..7e2d767b 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [features] geometry = ["lyon_path"] image = ["dep:image", "kamadak-exif"] +svg = [] web-colors = [] fira-sans = [] diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index ab4a7a36..2b4b45a6 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; +pub use crate::core::{Image, Svg}; pub use crate::gradient::{self, Gradient}; use crate::cache::Cached; diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 670fbc12..b79a2582 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -7,7 +7,7 @@ use crate::core::Color; use crate::gradient::{self, Gradient}; /// The style used to fill geometry. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Fill { /// The color or gradient of the fill. /// diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 377589d7..3dee7e75 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,6 +1,6 @@ //! Draw and generate geometry. use crate::core::{Point, Radians, Rectangle, Size, Vector}; -use crate::geometry::{self, Fill, Path, Stroke, Text}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; /// The region of a surface that can be used to draw geometry. #[allow(missing_debug_implementations)] @@ -65,6 +65,17 @@ where self.raw.stroke(path, stroke); } + /// Draws the stroke of an axis-aligned rectangle with the provided style + /// given its top-left corner coordinate and its `Size` on the [`Frame`] . + pub fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + /// Draws the characters of the given [`Text`] on the [`Frame`], filling /// them with the given color. /// @@ -75,6 +86,18 @@ where self.raw.fill_text(text); } + /// Draws the given [`Image`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "image")] + pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>) { + self.raw.draw_image(bounds, image); + } + + /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "svg")] + pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + self.raw.draw_svg(bounds, svg); + } + /// Stores the current transform of the [`Frame`] and executes the given /// drawing operations, restoring the transform afterwards. /// @@ -116,8 +139,7 @@ where let mut frame = self.draft(region); let result = f(&mut frame); - - self.paste(frame, Point::new(region.x, region.y)); + self.paste(frame); result } @@ -134,8 +156,8 @@ where } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - fn paste(&mut self, frame: Self, at: Point) { - self.raw.paste(frame.raw, at); + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); } /// Applies a translation to the current transform of the [`Frame`]. @@ -186,9 +208,15 @@ pub trait Backend: Sized { fn scale_nonuniform(&mut self, scale: impl Into<Vector>); fn draft(&mut self, clip_bounds: Rectangle) -> Self; - fn paste(&mut self, frame: Self, at: Point); + fn paste(&mut self, frame: Self); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ); fn fill(&mut self, path: &Path, fill: impl Into<Fill>); fn fill_text(&mut self, text: impl Into<Text>); @@ -199,6 +227,9 @@ pub trait Backend: Sized { fill: impl Into<Fill>, ); + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<Image>); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>); + fn into_geometry(self) -> Self::Geometry; } @@ -231,9 +262,16 @@ impl Backend for () { fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {} fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} - fn paste(&mut self, _frame: Self, _at: Point) {} + fn paste(&mut self, _frame: Self) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into<Stroke<'a>>, + ) { + } fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {} fn fill_text(&mut self, _text: impl Into<Text>) {} @@ -245,5 +283,8 @@ impl Backend for () { ) { } + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into<Image>) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into<Svg>) {} + fn into_geometry(self) -> Self::Geometry {} } diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index aff49ab3..b8f4515e 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -6,7 +6,7 @@ pub use crate::geometry::Style; use iced_core::Color; /// The style of a stroke. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Stroke<'a> { /// The color or gradient of the stroke. /// diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index a0f4b08a..de77eccc 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -2,7 +2,7 @@ use crate::core::Color; use crate::geometry::Gradient; /// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Style { /// A solid [`Color`]. Solid(Color), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85e..90147f87 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); @@ -50,8 +51,10 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, + None, + 4, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index 603f1b4a..54261721 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable}; use half::f16; use std::cmp::Ordering; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] /// A fill which linearly interpolates colors along a direction. /// /// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 318592be..67a5e0cf 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,57 +2,26 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Radians, Rectangle}; +use crate::core::image; +use crate::core::svg; +use crate::core::Rectangle; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, + Raster(image::Image, Rectangle), - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option<Color>, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, + Vector(svg::Svg, Rectangle), } impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { - bounds, rotation, .. - } - | Image::Vector { - bounds, rotation, .. - } => bounds.rotate(*rotation), + Image::Raster(image, bounds) => bounds.rotate(image.rotation), + Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), } } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 30269e69..feb9932a 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,7 +11,7 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrapping}; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; use once_cell::sync::OnceCell; @@ -232,13 +232,14 @@ impl PartialEq for Raw { /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { - let (width, total_lines) = buffer - .layout_runs() - .fold((0.0, 0usize), |(width, total_lines), run| { - (run.line_w.max(width), total_lines + 1) - }); + let (width, height) = + buffer + .layout_runs() + .fold((0.0, 0.0), |(width, height), run| { + (run.line_w.max(width), height + run.line_height) + }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + Size::new(width, height) } /// Returns the attributes of the given [`Font`]. @@ -305,6 +306,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 822b61c4..e64d93f1 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -48,8 +48,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c488a51c..1f1d0050 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -3,7 +3,7 @@ use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::LineHeight; +use crate::core::text::{LineHeight, Wrapping}; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -17,7 +17,7 @@ use std::sync::{self, Arc}; pub struct Editor(Option<Arc<Internal>>); struct Internal { - editor: cosmic_text::Editor, + editor: cosmic_text::Editor<'static>, font: Font, bounds: Size, topmost_line_changed: Option<usize>, @@ -32,7 +32,7 @@ impl Editor { /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + buffer_from_editor(&self.internal().editor) } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -82,6 +82,13 @@ impl editor::Editor for Editor { }))) } + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + fn line(&self, index: usize) -> Option<&str> { self.buffer() .lines @@ -101,16 +108,10 @@ impl editor::Editor for Editor { let internal = self.internal(); let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - - match internal.editor.select_opt() { - Some(selection) => { - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; + let buffer = buffer_from_editor(&internal.editor); + match internal.editor.selection_bounds() { + Some((start, end)) => { let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; @@ -142,7 +143,8 @@ impl editor::Editor for Editor { width, y: (visual_line as i32 + visual_lines_offset) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, height: line_height, }) } else { @@ -224,7 +226,8 @@ impl editor::Editor for Editor { Cursor::Caret(Point::new( offset, (visual_lines_offset + visual_line as i32) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, )) } } @@ -252,16 +255,8 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - let (left, right) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); match motion { // These motions are performed as-is even when a selection @@ -272,17 +267,20 @@ impl editor::Editor for Editor { | Motion::DocumentEnd => { editor.action( font_system.raw(), - motion_to_action(motion), + cosmic_text::Action::Motion(to_motion(motion)), ); } // Other motions simply move the cursor to one end of the selection _ => editor.set_cursor(match motion.direction() { - Direction::Left => left, - Direction::Right => right, + Direction::Left => start, + Direction::Right => end, }), } } else { - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); } } @@ -290,99 +288,58 @@ impl editor::Editor for Editor { Action::Select(motion) => { let cursor = editor.cursor(); - if editor.select_opt().is_none() { - editor.set_select_opt(Some(cursor)); + if editor.selection_bounds().is_none() { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); } - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } Action::SelectWord => { - use unicode_segmentation::UnicodeSegmentation; - let cursor = editor.cursor(); - if let Some(line) = editor.buffer().lines.get(cursor.line) { - let (start, end) = - UnicodeSegmentation::unicode_word_indices(line.text()) - // Split words with dots - .flat_map(|(i, word)| { - word.split('.').scan(i, |current, word| { - let start = *current; - *current += word.len() + 1; - - Some((start, word)) - }) - }) - // Turn words into ranges - .map(|(i, word)| (i, i + word.len())) - // Find the word at cursor - .find(|&(start, end)| { - start <= cursor.index && cursor.index < end - }) - // Cursor is not in a word. Let's select its punctuation cluster. - .unwrap_or_else(|| { - let start = line.text()[..cursor.index] - .char_indices() - .rev() - .take_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i) - .last() - .unwrap_or(cursor.index); - - let end = line.text()[cursor.index..] - .char_indices() - .skip_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i + cursor.index) - .next() - .unwrap_or(cursor.index); - - (start, end) - }); - - if start != end { - editor.set_cursor(cosmic_text::Cursor { - index: start, - ..cursor - }); - - editor.set_select_opt(Some(cosmic_text::Cursor { - index: end, - ..cursor - })); - } - } + editor.set_selection(cosmic_text::Selection::Word(cursor)); } Action::SelectLine => { let cursor = editor.cursor(); - if let Some(line_length) = editor - .buffer() - .lines - .get(cursor.line) - .map(|line| line.text().len()) + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); + + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) { - editor - .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); - editor.set_select_opt(Some(cosmic_text::Cursor { - index: line_length, - ..cursor - })); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), + ); } } @@ -419,10 +376,12 @@ impl editor::Editor for Editor { } let cursor = editor.cursor(); - let selection = editor.select_opt().unwrap_or(cursor); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); - internal.topmost_line_changed = - Some(cursor.min(selection).line); + internal.topmost_line_changed = Some(selection_start.line); } // Mouse events @@ -445,13 +404,9 @@ impl editor::Editor for Editor { ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } @@ -473,7 +428,7 @@ impl editor::Editor for Editor { fn min_bounds(&self) -> Size { let internal = self.internal(); - text::measure(internal.editor.buffer()) + text::measure(buffer_from_editor(&internal.editor)) } fn update( @@ -482,6 +437,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { let editor = @@ -493,10 +449,12 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); + let buffer = buffer_mut_from_editor(&mut internal.editor); + if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { line.reset(); } @@ -507,7 +465,7 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -517,7 +475,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = internal.editor.buffer().metrics(); + let metrics = buffer.metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -525,19 +483,27 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - internal.editor.buffer_mut().set_metrics( + buffer.set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); } + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(font_system.raw(), new_wrap); + } + if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - internal.editor.buffer_mut().set_size( + buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; @@ -552,7 +518,7 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -564,12 +530,13 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); + let buffer = buffer_from_editor(&internal.editor); - let mut window = buffer.scroll() + buffer.visible_lines(); + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; - let last_visible_line = buffer - .lines + let last_visible_line = buffer.lines[scroll.line..] .iter() .enumerate() .find_map(|(i, line)| { @@ -583,7 +550,7 @@ impl editor::Editor for Editor { window -= visible_lines; None } else { - Some(i) + Some(scroll.line + i) } }) .unwrap_or(buffer.lines.len().saturating_sub(1)); @@ -605,7 +572,7 @@ impl editor::Editor for Editor { let attributes = text::to_attributes(font); - for line in &mut internal.editor.buffer_mut().lines + for line in &mut buffer_mut_from_editor(&mut internal.editor).lines [current_line..=last_visible_line] { let mut list = cosmic_text::AttrsList::new(attributes); @@ -631,7 +598,7 @@ impl editor::Editor for Editor { let _ = line.set_attrs_list(list); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -647,7 +614,8 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.editor.buffer().metrics() == other.editor.buffer().metrics() + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() } } @@ -709,7 +677,8 @@ fn highlight_line( let layout = line .layout_opt() .as_ref() - .expect("Line layout should be cached"); + .map(Vec::as_slice) + .unwrap_or_default(); layout.iter().map(move |visual_line| { let start = visual_line @@ -752,34 +721,61 @@ fn highlight_line( } fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { - let visual_lines_before_start: usize = buffer - .lines + let scroll = buffer.scroll(); + + let start = scroll.line.min(line); + let end = scroll.line.max(line); + + let visual_lines_offset: usize = buffer.lines[start..] .iter() - .take(line) + .take(end - start) .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() + line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() }) .sum(); - visual_lines_before_start as i32 - buffer.scroll() + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } } -fn motion_to_action(motion: Motion) -> cosmic_text::Action { +fn to_motion(motion: Motion) -> cosmic_text::Motion { match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => cosmic_text::Action::BufferStart, - Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), } } 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) } } |