diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index 18ee393b4a..0bd2b5fb76 100644 --- a/examples/editor-libcosmic/src/main.rs +++ b/examples/editor-libcosmic/src/main.rs @@ -12,7 +12,7 @@ use cosmic::{ Element, }; use cosmic_text::{ - Align, Attrs, AttrsList, Buffer, Edit, FontSystem, Metrics, SyntaxEditor, SyntaxSystem, Wrap, + Align, Attrs, AttrsList, Buffer, Edit, FontSystem, LineHeight, SyntaxEditor, SyntaxSystem, Wrap, }; use std::{env, fmt, fs, path::PathBuf, sync::Mutex}; @@ -46,16 +46,33 @@ impl FontSize { ] } - pub fn to_metrics(self) -> Metrics { + pub fn to_font_size(self) -> f32 { match self { - Self::Caption => Metrics::new(10.0, 14.0), // Caption - Self::Body => Metrics::new(14.0, 20.0), // Body - Self::Title4 => Metrics::new(20.0, 28.0), // Title 4 - Self::Title3 => Metrics::new(24.0, 32.0), // Title 3 - Self::Title2 => Metrics::new(28.0, 36.0), // Title 2 - Self::Title1 => Metrics::new(32.0, 44.0), // Title 1 + Self::Caption => 10.0, // Caption + Self::Body => 14.0, // Body + Self::Title4 => 20.0, // Title 4 + Self::Title3 => 24.0, // Title 3 + Self::Title2 => 28.0, // Title 2 + Self::Title1 => 32.0, // Title 1 } } + + pub fn to_line_height(self) -> LineHeight { + match self { + Self::Caption => LineHeight::Absolute(14.0), // Caption + Self::Body => LineHeight::Absolute(20.0), // Body + Self::Title4 => LineHeight::Absolute(28.0), // Title 4 + Self::Title3 => LineHeight::Absolute(32.0), // Title 3 + Self::Title2 => LineHeight::Absolute(36.0), // Title 2 + Self::Title1 => LineHeight::Absolute(44.0), // Title 1 + } + } + + pub fn to_attrs(self) -> Attrs<'static> { + Attrs::new() + .font_size(self.to_font_size()) + .line_height(self.to_line_height()) + } } impl fmt::Display for FontSize { @@ -131,13 +148,12 @@ impl Application for Window { type Theme = Theme; fn new(_flags: ()) -> (Self, Command) { - let attrs = cosmic_text::Attrs::new().family(cosmic_text::Family::Monospace); + let attrs = FontSize::Body + .to_attrs() + .family(cosmic_text::Family::Monospace); let mut editor = SyntaxEditor::new( - Buffer::new( - &mut FONT_SYSTEM.lock().unwrap(), - FontSize::Body.to_metrics(), - ), + Buffer::new(&mut FONT_SYSTEM.lock().unwrap()), &SYNTAX_SYSTEM, "base16-eighties.dark", ) @@ -234,11 +250,13 @@ impl Application for Window { } Message::FontSizeChanged(font_size) => { self.font_size = font_size; + self.attrs = self + .attrs + .font_size(font_size.to_font_size()) + .line_height(font_size.to_line_height()); + let mut editor = self.editor.lock().unwrap(); - editor - .borrow_with(&mut FONT_SYSTEM.lock().unwrap()) - .buffer_mut() - .set_metrics(font_size.to_metrics()); + update_attrs(&mut *editor, self.attrs); } Message::WrapChanged(wrap) => { let mut editor = self.editor.lock().unwrap(); diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index b902e75593..00c59b2a89 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -138,15 +138,7 @@ where .buffer_mut() .shape_until(i32::max_value()); - let mut layout_lines = 0; - for line in editor.buffer().lines.iter() { - match line.layout_opt() { - Some(layout) => layout_lines += layout.len(), - None => (), - } - } - - let height = layout_lines as f32 * editor.buffer().metrics().line_height; + let height = editor.buffer().line_heights().iter().sum(); let size = Size::new(limits.max().width, height); log::info!("size {:?}", size); @@ -219,10 +211,10 @@ where let mut editor = editor.borrow_with(&mut font_system); // Scale metrics - let metrics = editor.buffer().metrics(); - editor - .buffer_mut() - .set_metrics(metrics.scale(SCALE_FACTOR as f32)); + // let metrics = editor.buffer().metrics(); + // editor + // .buffer_mut() + // .set_metrics(metrics.scale(SCALE_FACTOR as f32)); // Set size editor.buffer_mut().set_size(image_w as f32, image_h as f32); @@ -246,7 +238,7 @@ where ); // Restore original metrics - editor.buffer_mut().set_metrics(metrics); + // editor.buffer_mut().set_metrics(metrics); let handle = image::Handle::from_pixels(image_w as u32, image_h as u32, pixels); image::Renderer::draw( diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index a3d8c41ad1..801cdd9d6c 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use cosmic_text::{ - Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, Style, + Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, LineHeight, Shaping, Style, SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; @@ -36,9 +36,7 @@ fn main() { ) .unwrap(); - let mut editor = Editor::new(Buffer::new_empty( - Metrics::new(32.0, 44.0).scale(display_scale), - )); + let mut editor = Editor::new(Buffer::new_empty()); let mut editor = editor.borrow_with(&mut font_system); @@ -46,7 +44,10 @@ fn main() { .buffer_mut() .set_size(window.width() as f32, window.height() as f32); - let attrs = Attrs::new(); + let attrs = Attrs::new() + .font_size(32.0) + .line_height(LineHeight::Absolute(44.0)) + .scale(display_scale); let serif_attrs = attrs.family(Family::Serif); let mono_attrs = attrs.family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); @@ -97,13 +98,55 @@ fn main() { ("B", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), ("O", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), ("W ", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), - ("Red ", attrs.color(Color::rgb(0xFF, 0x00, 0x00))), - ("Orange ", attrs.color(Color::rgb(0xFF, 0x7F, 0x00))), - ("Yellow ", attrs.color(Color::rgb(0xFF, 0xFF, 0x00))), - ("Green ", attrs.color(Color::rgb(0x00, 0xFF, 0x00))), - ("Blue ", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), - ("Indigo ", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), - ("Violet ", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), + ( + "Red ", + attrs + .color(Color::rgb(0xFF, 0x00, 0x00)) + .font_size(attrs.font_size * 1.9) + .line_height(LineHeight::Proportional(0.9)), + ), + ( + "Orange ", + attrs + .color(Color::rgb(0xFF, 0x7F, 0x00)) + .font_size(attrs.font_size * 1.6) + .line_height(LineHeight::Proportional(1.0)), + ), + ( + "Yellow ", + attrs + .color(Color::rgb(0xFF, 0xFF, 0x00)) + .font_size(attrs.font_size * 1.3) + .line_height(LineHeight::Proportional(1.1)), + ), + ( + "Green ", + attrs + .color(Color::rgb(0x00, 0xFF, 0x00)) + .font_size(attrs.font_size * 1.0) + .line_height(LineHeight::Proportional(1.2)), + ), + ( + "Blue ", + attrs + .color(Color::rgb(0x00, 0x00, 0xFF)) + .font_size(attrs.font_size * 0.8) + .line_height(LineHeight::Proportional(1.3)), + ), + ( + "Indigo ", + attrs + .color(Color::rgb(0x4B, 0x00, 0x82)) + .font_size(attrs.font_size * 0.6) + .line_height(LineHeight::Proportional(1.4)), + ), + ( + "Violet ", + attrs + .color(Color::rgb(0x94, 0x00, 0xD3)) + .font_size(attrs.font_size * 0.4) + .line_height(LineHeight::Proportional(1.5)), + ), ("U", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), ("N", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), ("I", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), @@ -111,6 +154,12 @@ fn main() { ("O", attrs.color(Color::rgb(0xFF, 0xFF, 0x00))), ("R", attrs.color(Color::rgb(0xFF, 0x7F, 0x00))), ("N\n", attrs.color(Color::rgb(0xFF, 0x00, 0x00))), + ( + "\n", + attrs + .color(Color::rgb(0xFF, 0x00, 0x00)) + .line_height(LineHeight::Absolute(100.)), + ), ( "生活,삶,जिंदगी 😀 FPS\n", attrs.color(Color::rgb(0xFF, 0x00, 0x00)), diff --git a/rich-text.sh b/rich-text.sh index 3dfbd0ab53..b00488287d 100755 --- a/rich-text.sh +++ b/rich-text.sh @@ -1 +1 @@ -RUST_LOG=cosmic_text=debug,rich_text=debug cargo run --release --package rich-text -- "$@" +RUST_LOG=cosmic_text=debug,rich_text=debug,cosmic_text::shape=trace cargo run --release --package rich-text -- "$@" diff --git a/src/attrs.rs b/src/attrs.rs index e163124a24..f73fd1ec49 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -99,16 +99,62 @@ impl FamilyOwned { } } +/// Determines the line height and strategy. +/// The actual height of a line will be determined by the largest logical line height in a line. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineHeight { + /// Represents a line height that is proportional to the font size. + Proportional(f32), + /// Represents an absolute line height, independent of the font size. + Absolute(f32), +} + +impl core::hash::Hash for LineHeight { + fn hash(&self, state: &mut H) { + match self { + LineHeight::Proportional(height) => { + state.write_u8(1); + height.to_bits().hash(state); + } + LineHeight::Absolute(height) => { + state.write_u8(2); + height.to_bits().hash(state); + } + } + } +} + +impl LineHeight { + pub fn height(&self, font_size: f32) -> f32 { + match self { + LineHeight::Proportional(height) => *height * font_size, + LineHeight::Absolute(height) => *height, + } + } +} + +impl Default for LineHeight { + fn default() -> Self { + Self::Proportional(1.2) + } +} + /// Text attributes -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Attrs<'a> { //TODO: should this be an option? + // TODO: extract pub color_opt: Option, pub family: Family<'a>, pub stretch: Stretch, pub style: Style, pub weight: Weight, + // TODO: extract pub metadata: usize, + // TODO: extract + pub font_size: f32, + // TODO: extract + pub line_height: LineHeight, } impl<'a> Attrs<'a> { @@ -122,6 +168,8 @@ impl<'a> Attrs<'a> { stretch: Stretch::Normal, style: Style::Normal, weight: Weight::NORMAL, + font_size: 16.0, + line_height: LineHeight::Proportional(1.2), metadata: 0, } } @@ -132,6 +180,31 @@ impl<'a> Attrs<'a> { self } + /// Set font size + /// + /// # Panics + /// + /// Will panic if font size is zero. + pub fn font_size(mut self, size: f32) -> Self { + assert_ne!(size, 0.0, "font size cannot be 0"); + self.font_size = size; + self + } + + /// Set line height + /// + /// # Panics + /// + /// Will panic if line height is zero. + pub fn line_height(mut self, line_height: LineHeight) -> Self { + let inner = match line_height { + LineHeight::Absolute(inner) | LineHeight::Proportional(inner) => inner, + }; + assert_ne!(inner, 0.0, "line height cannot be 0"); + self.line_height = line_height; + self + } + /// Set [Family] pub fn family(mut self, family: Family<'a>) -> Self { self.family = family; @@ -178,18 +251,53 @@ impl<'a> Attrs<'a> { && self.style == other.style && self.weight == other.weight } + + /// Scale the font size and line height + /// + /// # Panics + /// + /// Will panic if scale is zero. + pub fn scale(mut self, scale: f32) -> Self { + assert_ne!(scale, 0.0, "scale cannot be 0"); + self.font_size = self.font_size * scale; + if let LineHeight::Absolute(height) = self.line_height { + self.line_height = LineHeight::Absolute(height * scale); + } + self + } +} + +impl<'a> Eq for Attrs<'a> {} + +impl<'a> core::hash::Hash for Attrs<'a> { + fn hash(&self, state: &mut H) { + self.color_opt.hash(state); + self.family.hash(state); + self.stretch.hash(state); + self.style.hash(state); + self.weight.hash(state); + self.metadata.hash(state); + self.font_size.to_bits().hash(state); + self.line_height.hash(state); + } } /// An owned version of [`Attrs`] -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AttrsOwned { //TODO: should this be an option? + // TODO: extract pub color_opt: Option, pub family_owned: FamilyOwned, pub stretch: Stretch, pub style: Style, pub weight: Weight, + // TODO: extract pub metadata: usize, + // TODO: extract + pub font_size: f32, + // TODO: extract + pub line_height: LineHeight, } impl AttrsOwned { @@ -201,6 +309,8 @@ impl AttrsOwned { style: attrs.style, weight: attrs.weight, metadata: attrs.metadata, + font_size: attrs.font_size, + line_height: attrs.line_height, } } @@ -212,10 +322,27 @@ impl AttrsOwned { style: self.style, weight: self.weight, metadata: self.metadata, + font_size: self.font_size, + line_height: self.line_height, } } } +impl Eq for AttrsOwned {} + +impl core::hash::Hash for AttrsOwned { + fn hash(&self, state: &mut H) { + self.color_opt.hash(state); + self.family_owned.hash(state); + self.stretch.hash(state); + self.style.hash(state); + self.weight.hash(state); + self.metadata.hash(state); + self.font_size.to_bits().hash(state); + self.line_height.hash(state); + } +} + /// List of text attributes to apply to a line //TODO: have this clean up the spans when changes are made #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/src/buffer.rs b/src/buffer.rs index fc285b5897..ba20b5c538 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,8 +1,22 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 +//! This module contains the [`Buffer`] type which is the entry point for shaping and layout of text. +//! +//! A [`Buffer`] contains a list of [`BufferLine`]s and is used to compute the [`LayoutRun`]s. +//! +//! [`BufferLine`]s correspond to the paragraphs of text in the [`Buffer`]. +//! Each [`BufferLine`] contains a list of [`LayoutLine`]s, which represent the visual lines. +//! `Buffer::line_heights` is a computed list of visual line heights. +//! +//! [`LayoutRun`]s represent the actually-visible visual lines, +//! based on the [`Buffer`]'s scroll position, width, height and wrapping mode. + #[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; -use core::{cmp, fmt}; +use alloc::{ + borrow::ToOwned, + string::{String, ToString}, + vec::Vec, +}; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -105,9 +119,9 @@ impl LayoutCursor { /// A line of visible text for rendering #[derive(Debug)] pub struct LayoutRun<'a> { - /// The index of the original text line + /// The index of the original [`BufferLine`] (or paragraph) in the [`Buffer`] pub line_i: usize, - /// The original text line + /// The text of the original [`BufferLine`] (or paragraph) pub text: &'a str, /// True if the original paragraph direction is RTL pub rtl: bool, @@ -119,6 +133,8 @@ pub struct LayoutRun<'a> { pub line_top: f32, /// Width of line pub line_w: f32, + /// The height of the line + pub line_height: f32, } impl<'a> LayoutRun<'a> { @@ -189,30 +205,16 @@ pub struct LayoutRunIter<'b> { impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { - let total_layout_lines: usize = buffer - .lines - .iter() - .map(|line| { - line.layout_opt() - .as_ref() - .map(|layout| layout.len()) - .unwrap_or_default() - }) - .sum(); + let line_heights = buffer.line_heights(); + let total_layout_lines = line_heights.len(); let top_cropped_layout_lines = total_layout_lines.saturating_sub(buffer.scroll.try_into().unwrap_or_default()); - let maximum_lines = if buffer.metrics.line_height == 0.0 { - 0 + let maximum_lines: usize = buffer.visible_lines().try_into().unwrap_or_default(); + let bottom_cropped_layout_lines = if top_cropped_layout_lines > maximum_lines { + maximum_lines } else { - (buffer.height / buffer.metrics.line_height) as i32 + top_cropped_layout_lines }; - let bottom_cropped_layout_lines = - if top_cropped_layout_lines > maximum_lines.try_into().unwrap_or_default() { - maximum_lines.try_into().unwrap_or_default() - } else { - top_cropped_layout_lines - }; - Self { buffer, line_i: 0, @@ -233,7 +235,7 @@ impl<'b> Iterator for LayoutRunIter<'b> { fn next(&mut self) -> Option { while let Some(line) = self.buffer.lines.get(self.line_i) { let shape = line.shape_opt().as_ref()?; - let layout = line.layout_opt().as_ref()?; + let layout = line.layout_opt()?; while let Some(layout_line) = layout.get(self.layout_i) { self.layout_i += 1; @@ -242,14 +244,13 @@ impl<'b> Iterator for LayoutRunIter<'b> { if scrolled { continue; } - - let line_top = self - .total_layout - .saturating_sub(self.buffer.scroll) - .saturating_sub(1) as f32 - * self.buffer.metrics.line_height; + // TODO: can scroll be negative? + let this_line = self.total_layout.saturating_sub(1) as usize; + let line_top = self.buffer.line_heights[self.buffer.scroll as usize..this_line] + .iter() + .sum(); let glyph_height = layout_line.max_ascent + layout_line.max_descent; - let centering_offset = (self.buffer.metrics.line_height - glyph_height) / 2.0; + let centering_offset = (self.buffer.line_heights[this_line] - glyph_height) / 2.0; let line_y = line_top + centering_offset + layout_line.max_ascent; if line_top + centering_offset > self.buffer.height { @@ -265,7 +266,8 @@ impl<'b> Iterator for LayoutRunIter<'b> { glyphs: &layout_line.glyphs, line_y, line_top, - line_w: layout_line.w, + line_w: layout_line.width, + line_height: self.buffer.line_heights[this_line], } }); } @@ -279,71 +281,39 @@ impl<'b> Iterator for LayoutRunIter<'b> { impl<'b> ExactSizeIterator for LayoutRunIter<'b> {} -/// Metrics of text -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct Metrics { - /// Font size in pixels - pub font_size: f32, - /// Line height in pixels - pub line_height: f32, -} - -impl Metrics { - pub const fn new(font_size: f32, line_height: f32) -> Self { - Self { - font_size, - line_height, - } - } - - pub fn scale(self, scale: f32) -> Self { - Self { - font_size: self.font_size * scale, - line_height: self.line_height * scale, - } - } -} - -impl fmt::Display for Metrics { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}px / {}px", self.font_size, self.line_height) - } -} - /// A buffer of text that is shaped and laid out #[derive(Debug)] pub struct Buffer { - /// [BufferLine]s (or paragraphs) of text in the buffer + /// [BufferLine]s (or paragraphs) of text in the buffer. pub lines: Vec, - metrics: Metrics, + /// The cached heights of visual lines. Compute using [`Buffer::update_line_heights`]. + line_heights: Vec, + /// The text bounding box width. width: f32, + /// The text bounding box height. height: f32, + /// The current scroll position in terms of visual lines. scroll: i32, - /// True if a redraw is requires. Set to false after processing + /// True if a redraw is required. Set to false after processing. redraw: bool, + /// The wrapping mode wrap: Wrap, - /// Scratch buffer for shaping and laying out. scratch: ShapeBuffer, } impl Buffer { - /// Create an empty [`Buffer`] with the provided [`Metrics`]. + /// Create an empty [`Buffer`] /// This is useful for initializing a [`Buffer`] without a [`FontSystem`]. /// /// You must populate the [`Buffer`] with at least one [`BufferLine`] before shaping and layout, /// for example by calling [`Buffer::set_text`]. /// /// If you have a [`FontSystem`] in scope, you should use [`Buffer::new`] instead. - /// - /// # Panics - /// - /// Will panic if `metrics.line_height` is zero. - pub fn new_empty(metrics: Metrics) -> Self { - assert_ne!(metrics.line_height, 0.0, "line height cannot be 0"); + pub fn new_empty() -> Self { Self { lines: Vec::new(), - metrics, + line_heights: Vec::new(), width: 0.0, height: 0.0, scroll: 0, @@ -353,13 +323,9 @@ impl Buffer { } } - /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`] - /// - /// # Panics - /// - /// Will panic if `metrics.line_height` is zero. - pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self { - let mut buffer = Self::new_empty(metrics); + /// Create a new [`Buffer`] with the provided [`FontSystem`] + pub fn new(font_system: &mut FontSystem) -> Self { + let mut buffer = Self::new_empty(); buffer.set_text(font_system, "", Attrs::new(), Shaping::Advanced); buffer } @@ -382,16 +348,43 @@ impl Buffer { for line in &mut self.lines { if line.shape_opt().is_some() { line.reset_layout(); - line.layout(font_system, self.metrics.font_size, self.width, self.wrap); + line.layout(font_system, self.width, self.wrap); } } + self.update_line_heights(); self.redraw = true; #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!("relayout: {:?}", instant.elapsed()); } + /// Get the cached heights of visual lines + pub fn line_heights(&self) -> &[f32] { + self.line_heights.as_slice() + } + + /// Update the cached heights of visual lines + pub fn update_line_heights(&mut self) { + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + let instant = std::time::Instant::now(); + + self.line_heights.clear(); + let iter = self + .lines + .iter() + .flat_map(|line| line.line_heights()) + .flat_map(|lines| lines.iter().copied()); + self.line_heights.extend(iter); + + #[cfg(all(feature = "std", not(target_arch = "wasm32")))] + log::debug!( + "update_line_heights {}: {:?}", + self.line_heights.len(), + instant.elapsed() + ); + } + /// Pre-shape lines in the buffer, up to `lines`, return actual number of layout lines pub fn shape_until(&mut self, font_system: &mut FontSystem, lines: i32) -> i32 { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] @@ -399,6 +392,7 @@ impl Buffer { let mut reshaped = 0; let mut total_layout = 0; + let mut should_update_line_heights = false; for line in &mut self.lines { if total_layout >= lines { break; @@ -407,16 +401,20 @@ impl Buffer { if line.shape_opt().is_none() { reshaped += 1; } - let layout = line.layout_in_buffer( - &mut self.scratch, - font_system, - self.metrics.font_size, - self.width, - self.wrap, - ); + + if line.layout_opt().is_none() { + should_update_line_heights = true; + } + + let layout = + line.layout_in_buffer(&mut self.scratch, font_system, self.width, self.wrap); total_layout += layout.len() as i32; } + if should_update_line_heights { + self.update_line_heights(); + } + if reshaped > 0 { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!("shape_until {}: {:?}", reshaped, instant.elapsed()); @@ -433,6 +431,7 @@ impl Buffer { let mut reshaped = 0; let mut layout_i = 0; + let mut should_update_line_heights = false; for (line_i, line) in self.lines.iter_mut().enumerate() { if line_i > cursor.line { break; @@ -441,13 +440,13 @@ impl Buffer { if line.shape_opt().is_none() { reshaped += 1; } - let layout = line.layout_in_buffer( - &mut self.scratch, - font_system, - self.metrics.font_size, - self.width, - self.wrap, - ); + + if line.layout_opt().is_none() { + should_update_line_heights = true; + } + + let layout = + line.layout_in_buffer(&mut self.scratch, font_system, self.width, self.wrap); if line_i == cursor.line { let layout_cursor = self.layout_cursor(&cursor); layout_i += layout_cursor.layout as i32; @@ -457,16 +456,24 @@ impl Buffer { } } + if should_update_line_heights { + self.update_line_heights(); + } + if reshaped > 0 { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!("shape_until_cursor {}: {:?}", reshaped, instant.elapsed()); self.redraw = true; } + // the first visible line is index = self.scroll + // the last visible line is index = self.scroll + lines let lines = self.visible_lines(); if layout_i < self.scroll { self.scroll = layout_i; } else if layout_i >= self.scroll + lines { + // need to work backwards from layout_i using the line heights + let lines = self.visible_lines_to(layout_i as usize); self.scroll = layout_i - (lines - 1); } @@ -475,19 +482,20 @@ impl Buffer { /// Shape lines until scroll pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem) { + self.layout_lines(font_system); + let lines = self.visible_lines(); let scroll_end = self.scroll + lines; let total_layout = self.shape_until(font_system, scroll_end); - - self.scroll = cmp::max(0, cmp::min(total_layout - lines, self.scroll)); + self.scroll = (total_layout - lines).clamp(0, self.scroll); } pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor { let line = &self.lines[cursor.line]; //TODO: ensure layout is done? - let layout = line.layout_opt().as_ref().expect("layout not found"); + let layout = line.layout_opt().expect("layout not found"); for (layout_i, layout_line) in layout.iter().enumerate() { for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() { let cursor_end = @@ -529,22 +537,41 @@ impl Buffer { font_system: &mut FontSystem, line_i: usize, ) -> Option<&[LayoutLine]> { + let should_update_line_heights = { + let line = self.lines.get_mut(line_i)?; + // check if the line needs to be laid out + if line.layout_opt().is_none() { + // update the layout (result will be cached) + let _ = line.layout(font_system, self.width, self.wrap); + true + } else { + false + } + }; + + if should_update_line_heights { + self.update_line_heights(); + } + let line = self.lines.get_mut(line_i)?; - Some(line.layout(font_system, self.metrics.font_size, self.width, self.wrap)) - } - /// Get the current [`Metrics`] - pub fn metrics(&self) -> Metrics { - self.metrics + // return cached layout + Some(line.layout(font_system, self.width, self.wrap)) } - /// Set the current [`Metrics`] - /// - /// # Panics - /// - /// Will panic if `metrics.font_size` is zero. - pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { - self.set_metrics_and_size(font_system, metrics, self.width, self.height); + /// Lay out all lines without shaping + pub fn layout_lines(&mut self, font_system: &mut FontSystem) { + let mut should_update_line_heights = false; + for line in self.lines.iter_mut() { + if line.layout_opt().is_none() { + should_update_line_heights = true; + let _ = line.layout(font_system, self.width, self.wrap); + } + } + + if should_update_line_heights { + self.update_line_heights(); + } } /// Get the current [`Wrap`] @@ -568,50 +595,74 @@ impl Buffer { /// Set the current buffer dimensions pub fn set_size(&mut self, font_system: &mut FontSystem, width: f32, height: f32) { - self.set_metrics_and_size(font_system, self.metrics, width, height); - } - - /// Set the current [`Metrics`] and buffer dimensions at the same time - /// - /// # Panics - /// - /// Will panic if `metrics.font_size` is zero. - pub fn set_metrics_and_size( - &mut self, - font_system: &mut FontSystem, - metrics: Metrics, - width: f32, - height: f32, - ) { let clamped_width = width.max(0.0); let clamped_height = height.max(0.0); - - if metrics != self.metrics || clamped_width != self.width || clamped_height != self.height { - assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); - self.metrics = metrics; - self.width = clamped_width; - self.height = clamped_height; - self.relayout(font_system); - self.shape_until_scroll(font_system); - } + self.width = clamped_width; + self.height = clamped_height; + self.relayout(font_system); + self.shape_until_scroll(font_system); } - /// Get the current scroll location + /// Get the current scroll location in terms of visual lines pub fn scroll(&self) -> i32 { self.scroll } - /// Set the current scroll location + /// Set the current scroll location in terms of visual lines. + /// + /// This is clamped to the visual lines of the buffer. pub fn set_scroll(&mut self, scroll: i32) { + let visual_lines = self.line_heights().len() as i32; + let scroll = scroll.clamp(0, visual_lines - 1); if scroll != self.scroll { self.scroll = scroll; self.redraw = true; } } - /// Get the number of lines that can be viewed in the buffer + /// Get the number of lines that can be viewed in the buffer, from a starting point + pub fn visible_lines_from(&self, from: usize) -> i32 { + let mut height = self.height; + let line_heights = self.line_heights(); + if line_heights.is_empty() { + // this has never been laid out, so we can't know the height yet + return i32::MAX; + } + let mut i = 0; + let mut iter = line_heights.iter().skip(from); + while let Some(line_height) = iter.next() { + height -= line_height; + if height <= 0.0 { + break; + } + i += 1; + } + i + } + + /// Get the number of lines that can be viewed in the buffer, to an ending point + pub fn visible_lines_to(&self, to: usize) -> i32 { + let mut height = self.height; + let line_heights = self.line_heights(); + if line_heights.is_empty() { + // this has never been laid out, so we can't know the height yet + return i32::MAX; + } + let mut i = 0; + let mut iter = line_heights.iter().rev().skip(line_heights.len() - to - 1); + while let Some(line_height) = iter.next() { + height -= line_height; + if height <= 0.0 { + break; + } + i += 1; + } + i + } + + /// Get the number of visual lines that can be viewed in the buffer pub fn visible_lines(&self) -> i32 { - (self.height / self.metrics.line_height) as i32 + self.visible_lines_from(self.scroll as usize) } /// Set text of buffer, using provided attributes for each line by default @@ -628,9 +679,9 @@ impl Buffer { /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// /// ``` - /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Shaping}; /// # let mut font_system = FontSystem::new(); - /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); + /// let mut buffer = Buffer::new_empty(); /// let attrs = Attrs::new().family(Family::Serif); /// buffer.set_rich_text( /// &mut font_system, @@ -713,8 +764,20 @@ impl Buffer { maybe_line = lines_iter.next(); if maybe_line.is_some() { // finalize this line and start a new line - let prev_attrs_list = - core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); + + let attr_list = match &maybe_span { + // there can be newlines, that has their own span attributes, "\n" lines for example, thus add + // their spans if they need it + Some((attr, range)) => { + let mut list = AttrsList::new(default_attrs); + if *attr != attrs_list.defaults() { + list.add_span(range.clone(), attr.to_owned()); + } + list + } + None => AttrsList::new(default_attrs), + }; + let prev_attrs_list = core::mem::replace(&mut attrs_list, attr_list); let prev_line_string = core::mem::take(&mut line_string); let buffer_line = BufferLine::new(prev_line_string, prev_attrs_list, shaping); self.lines.push(buffer_line); @@ -748,104 +811,148 @@ impl Buffer { } /// Convert x, y position to Cursor (hit detection) - pub fn hit(&self, x: f32, y: f32) -> Option { + pub fn hit(&self, strike_x: f32, strike_y: f32) -> Option { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); - let font_size = self.metrics.font_size; - let line_height = self.metrics.line_height; - - let mut new_cursor_opt = None; - - let mut runs = self.layout_runs().peekable(); - let mut first_run = true; - while let Some(run) = runs.next() { - let line_y = run.line_y; - - if first_run && y < line_y - font_size { - first_run = false; - let new_cursor = Cursor::new(run.line_i, 0); - new_cursor_opt = Some(new_cursor); - } else if y >= line_y - font_size && y < line_y - font_size + line_height { - let mut new_cursor_glyph = run.glyphs.len(); - let mut new_cursor_char = 0; - let mut new_cursor_affinity = Affinity::After; - - let mut first_glyph = true; - - 'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() { - if first_glyph { - first_glyph = false; - if (run.rtl && x > glyph.x) || (!run.rtl && x < 0.0) { - new_cursor_glyph = 0; - new_cursor_char = 0; - } + // below, + // - `first`, `last` refers to iterator indices (usize) + // - `start`, `end` refers to byte indices (usize) + // - `left`, `top`, `right`, `bot`, `mid` refers to spatial coordinates (f32) + + let Some(last_run_index) = self.layout_runs().count().checked_sub(1) else { + return None; + }; + + let mut runs = self.layout_runs().enumerate(); + + // TODO: consider caching line_top and line_bot on LayoutRun + + // 1. within the buffer, find the layout run (line) that contains the strike point + // 2. within the layout run (line), find the glyph that contains the strike point + // 3. within the glyph, find the approximate extended grapheme cluster (egc) that contains the strike point + // the boundary (top/bot, left/right) cases in each step are special + let mut line_top = 0.0; + let cursor = 'hit: loop { + let Some((run_index, run)) = runs.next() else { + // no hit found + break 'hit None; + }; + + if run_index == 0 && strike_y < line_top { + // hit above top line + break 'hit Some(Cursor::new(run.line_i, 0)); + } + + let line_bot = line_top + run.line_height; + + if run_index == last_run_index && strike_y >= line_bot { + // hit below bottom line + match run.glyphs.last() { + Some(glyph) => break 'hit Some(run.cursor_from_glyph_right(glyph)), + None => break 'hit Some(Cursor::new(run.line_i, 0)), + } + } + + if (line_top..line_bot).contains(&strike_y) { + let last_glyph_index = run.glyphs.len() - 1; + + // TODO: is this assumption correct with rtl? + let (left_glyph_index, right_glyph_index) = if run.rtl { + (last_glyph_index, 0) + } else { + (0, last_glyph_index) + }; + + for (glyph_index, glyph) in run.glyphs.iter().enumerate() { + let glyph_left = glyph.x; + + if glyph_index == left_glyph_index && strike_x < glyph_left { + // hit left of left-most glyph in line + break 'hit Some(Cursor::new(run.line_i, 0)); + } + + let glyph_right = glyph_left + glyph.w; + + if glyph_index == right_glyph_index && strike_x >= glyph_right { + // hit right of right-most glyph in line + break 'hit Some(run.cursor_from_glyph_right(glyph)); } - if x >= glyph.x && x <= glyph.x + glyph.w { - new_cursor_glyph = glyph_i; + if (glyph_left..glyph_right).contains(&strike_x) { let cluster = &run.text[glyph.start..glyph.end]; - let total = cluster.grapheme_indices(true).count(); - let mut egc_x = glyph.x; + + let total = cluster.graphemes(true).count(); + let last_egc_index = total - 1; let egc_w = glyph.w / (total as f32); - for (egc_i, egc) in cluster.grapheme_indices(true) { - if x >= egc_x && x <= egc_x + egc_w { - new_cursor_char = egc_i; - - let right_half = x >= egc_x + egc_w / 2.0; - if right_half != glyph.level.is_rtl() { - // If clicking on last half of glyph, move cursor past glyph - new_cursor_char += egc.len(); - new_cursor_affinity = Affinity::Before; - } - break 'hit; + let mut egc_left = glyph_left; + + // TODO: is this assumption correct with rtl? + let (left_egc_index, right_egc_index) = if glyph.level.is_rtl() { + (last_egc_index, 0) + } else { + (0, last_egc_index) + }; + + for (egc_index, (egc_start, egc)) in + cluster.grapheme_indices(true).enumerate() + { + let egc_end = egc_start + egc.len(); + + let (left_egc_byte, right_egc_byte) = if glyph.level.is_rtl() { + (glyph.start + egc_end, glyph.start + egc_start) + } else { + (glyph.start + egc_start, glyph.start + egc_end) + }; + + if egc_index == left_egc_index && strike_x < egc_left { + // hit left of left-most egc in cluster + break 'hit Some(Cursor::new(run.line_i, left_egc_byte)); } - egc_x += egc_w; - } - let right_half = x >= glyph.x + glyph.w / 2.0; - if right_half != glyph.level.is_rtl() { - // If clicking on last half of glyph, move cursor past glyph - new_cursor_char = cluster.len(); - new_cursor_affinity = Affinity::Before; - } - break 'hit; - } - } + let egc_right = egc_left + egc_w; - let mut new_cursor = Cursor::new(run.line_i, 0); + if egc_index == right_egc_index && strike_x >= egc_right { + // hit right of right-most egc in cluster + break 'hit Some(Cursor::new(run.line_i, right_egc_byte)); + } - match run.glyphs.get(new_cursor_glyph) { - Some(glyph) => { - // Position at glyph - new_cursor.index = glyph.start + new_cursor_char; - new_cursor.affinity = new_cursor_affinity; - } - None => { - if let Some(glyph) = run.glyphs.last() { - // Position at end of line - new_cursor.index = glyph.end; - new_cursor.affinity = Affinity::Before; + let egc_mid = egc_left + egc_w / 2.0; + + let hit_egc = if (egc_left..egc_mid).contains(&strike_x) { + // hit left half of egc + Some(true) + } else if (egc_mid..egc_right).contains(&strike_x) { + // hit right half of egc + Some(false) + } else { + None + }; + + if let Some(egc_left_half) = hit_egc { + break 'hit Some(Cursor::new( + run.line_i, + if egc_left_half { + left_egc_byte + } else { + right_egc_byte + }, + )); + } + + egc_left = egc_right; } } } - - new_cursor_opt = Some(new_cursor); - - break; - } else if runs.peek().is_none() && y > run.line_y { - let mut new_cursor = Cursor::new(run.line_i, 0); - if let Some(glyph) = run.glyphs.last() { - new_cursor = run.cursor_from_glyph_right(glyph); - } - new_cursor_opt = Some(new_cursor); } - } + + line_top = line_bot; + }; #[cfg(all(feature = "std", not(target_arch = "wasm32")))] - log::trace!("click({}, {}): {:?}", x, y, instant.elapsed()); + log::trace!("click({}, {}): {:?}", strike_x, strike_y, instant.elapsed()); - new_cursor_opt + cursor } /// Draw the buffer @@ -913,15 +1020,6 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { self.inner.line_layout(self.font_system, line_i) } - /// Set the current [`Metrics`] - /// - /// # Panics - /// - /// Will panic if `metrics.font_size` is zero. - pub fn set_metrics(&mut self, metrics: Metrics) { - self.inner.set_metrics(self.font_system, metrics); - } - /// Set the current [`Wrap`] pub fn set_wrap(&mut self, wrap: Wrap) { self.inner.set_wrap(self.font_system, wrap); @@ -932,16 +1030,6 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { self.inner.set_size(self.font_system, width, height); } - /// Set the current [`Metrics`] and buffer dimensions at the same time - /// - /// # Panics - /// - /// Will panic if `metrics.font_size` is zero. - pub fn set_metrics_and_size(&mut self, metrics: Metrics, width: f32, height: f32) { - self.inner - .set_metrics_and_size(self.font_system, metrics, width, height); - } - /// Set text of buffer, using provided attributes for each line by default pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) { self.inner.set_text(self.font_system, text, attrs, shaping); @@ -950,9 +1038,9 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// /// ``` - /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Shaping}; /// # let mut font_system = FontSystem::new(); - /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); + /// let mut buffer = Buffer::new_empty(); /// let mut buffer = buffer.borrow_with(&mut font_system); /// let attrs = Attrs::new().family(Family::Serif); /// buffer.set_rich_text( diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 667cc80cd6..c3e948bff6 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -12,7 +12,7 @@ pub struct BufferLine { wrap: Wrap, align: Option, shape_opt: Option, - layout_opt: Option>, + layout_opt: Option, shaping: Shaping, } @@ -197,45 +197,70 @@ impl BufferLine { } /// Layout line, will cache results + /// + /// Ensure that if this buffer line was laid out, you call [`Buffer::update_line_heights`] afterwards pub fn layout( &mut self, font_system: &mut FontSystem, - font_size: f32, width: f32, wrap: Wrap, ) -> &[LayoutLine] { - if self.layout_opt.is_none() { - self.wrap = wrap; - let align = self.align; - let shape = self.shape(font_system); - let layout = shape.layout(font_size, width, wrap, align); - self.layout_opt = Some(layout); - } - self.layout_opt.as_ref().expect("layout not found") + self.layout_in_buffer(&mut ShapeBuffer::default(), font_system, width, wrap) } /// Layout a line using a pre-existing shape buffer. + /// + /// Ensure that if this buffer line was laid out, you call [`Buffer::update_line_heights`] afterwards pub fn layout_in_buffer( &mut self, scratch: &mut ShapeBuffer, font_system: &mut FontSystem, - font_size: f32, width: f32, wrap: Wrap, ) -> &[LayoutLine] { if self.layout_opt.is_none() { self.wrap = wrap; let align = self.align; - let shape = self.shape_in_buffer(scratch, font_system); + let mut layout = Vec::with_capacity(1); - shape.layout_to_buffer(scratch, font_size, width, wrap, align, &mut layout); - self.layout_opt = Some(layout); + let empty_height = if let Some(span) = &self.attrs_list().spans().first() { + span.1.line_height.height(span.1.font_size) + } else { + //TODO: figure out what empty lines without any span info should do.. previosly they defaulted to zero + let attrs = self.attrs_list().defaults(); + attrs.line_height.height(attrs.font_size) + }; + let shape = self.shape_in_buffer(scratch, font_system); + shape.layout_to_buffer(scratch, width, wrap, align, &mut layout, empty_height); + + let line_heights = layout.iter().map(|line| line.line_height()).collect(); + + self.layout_opt = Some(LayoutLines { + layout, + line_heights, + }); } - self.layout_opt.as_ref().expect("layout not found") + self.layout_opt + .as_ref() + .map(|l| l.layout.as_ref()) + .expect("layout not found") } /// Get line layout cache - pub fn layout_opt(&self) -> &Option> { - &self.layout_opt + pub fn layout_opt(&self) -> Option<&[LayoutLine]> { + self.layout_opt.as_ref().map(|l| l.layout.as_ref()) + } + + /// Get line height cache + pub fn line_heights(&self) -> Option<&[f32]> { + let r = self.layout_opt.as_ref().map(|l| l.line_heights.as_ref()); + r } } + +/// A list of [`LayoutLine`] in a [`BufferLine`] alongside their line heights +#[derive(Debug)] +struct LayoutLines { + layout: Vec, + line_heights: Vec, +} diff --git a/src/edit/editor.rs b/src/edit/editor.rs index fd468c301e..4388e363a6 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -229,20 +229,22 @@ impl Edit for Editor { // we want to see a blank entry if the string ends with a newline let addendum = once("").filter(|_| data.ends_with('\n')); let mut lines_iter = data.split_inclusive('\n').chain(addendum); - if let Some(data_line) = lines_iter.next() { - let mut these_attrs = final_attrs.split_off(data_line.len()); - remaining_split_len -= data_line.len(); - core::mem::swap(&mut these_attrs, &mut final_attrs); - line.append(BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - these_attrs, - Shaping::Advanced, - )); - } else { - panic!("str::lines() did not yield any elements"); - } + + let data_line = lines_iter + .next() + .expect("str::lines() did not yield any elements"); + + let mut these_attrs = final_attrs.split_off(data_line.len()); + remaining_split_len -= data_line.len(); + core::mem::swap(&mut these_attrs, &mut final_attrs); + line.append(BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + these_attrs, + Shaping::Advanced, + )); + if let Some(data_line) = lines_iter.next_back() { remaining_split_len -= data_line.len(); let mut tmp = BufferLine::new( @@ -537,22 +539,53 @@ impl Edit for Editor { Action::PageDown => { self.action(font_system, Action::Vertical(self.buffer.size().1 as i32)); } - Action::Vertical(px) => { + Action::Vertical(mut px) => { // TODO more efficient - let lines = px / self.buffer.metrics().line_height as i32; - match lines.cmp(&0) { - cmp::Ordering::Less => { - for _ in 0..-lines { + let cursor = self.buffer.layout_cursor(&self.cursor); + let mut current_line = cursor.line as i32; + let direction = px.signum(); + loop { + current_line += direction; + if current_line < 0 || current_line >= self.buffer.line_heights().len() as i32 { + break; + } + + let current_line_height = self.buffer.line_heights()[current_line as usize]; + + match direction { + -1 => { self.action(font_system, Action::Up); + px -= current_line_height as i32; + if px >= self.buffer.size().1 as i32 { + break; + } } - } - cmp::Ordering::Greater => { - for _ in 0..lines { + 1 => { self.action(font_system, Action::Down); + px += current_line_height as i32; + + if px <= 0 as i32 { + break; + } } + _ => break, } - cmp::Ordering::Equal => {} } + // let lines = px / self.buffer.metrics().line_height as i32; + // match lines.cmp(&0) { + // Ordering::Less => { + // for _ in 0..-lines { + // self.action(font_system, Action::Up); + // } + // } + // Ordering::Greater => { + // for _ in 0..lines { + // self.action(font_system, Action::Down); + // } + // } + // Ordering::Equal => {} + // } + self.buffer.set_redraw(true); } Action::Escape => { match self.selection { @@ -778,6 +811,8 @@ impl Edit for Editor { // Request redraw self.buffer.set_redraw(true); } + // TODO: Delete doesn't redraw without this now + self.buffer.set_redraw(true); } Action::Click { x, y } => { self.set_selection(Selection::None); @@ -923,12 +958,11 @@ impl Edit for Editor { ) where F: FnMut(i32, i32, u32, u32, Color), { - let line_height = self.buffer.metrics().line_height; - for run in self.buffer.layout_runs() { let line_i = run.line_i; let line_y = run.line_y; let line_top = run.line_top; + let line_height = run.line_height; let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> { if cursor.line == line_i { diff --git a/src/layout.rs b/src/layout.rs index 382522624a..dfc6d8f2b6 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -16,6 +16,8 @@ pub struct LayoutGlyph { pub end: usize, /// Font size of the glyph pub font_size: f32, + /// Line height + pub line_height: f32, /// Font id of the glyph pub font_id: fontdb::ID, /// Font id of the glyph @@ -85,13 +87,54 @@ impl LayoutGlyph { #[derive(Clone, Debug)] pub struct LayoutLine { /// Width of the line - pub w: f32, + pub width: f32, /// Maximum ascent of the glyphs in line pub max_ascent: f32, /// Maximum descent of the glyphs in line pub max_descent: f32, /// Glyphs in line pub glyphs: Vec, + /// Height of the line, calculated from the glyphs at creation + height: f32, +} + +impl LayoutLine { + /// Creates a new layoutline and calculates the line height from the largest characters in the line, + // if the line has no characters, the height will be zero + pub fn new(width: f32, max_ascent: f32, max_descent: f32, glyphs: Vec) -> Self { + // Calculates the line height from the largest characters in the line, if the line has no characters + // well the line height is 0 + let height = glyphs + .iter() + .map(|g| g.line_height) + .reduce(f32::max) + .unwrap_or(0.0); + Self { + width, + max_ascent, + max_descent, + glyphs, + height, + } + } + /// generates an empty layoutline with only a height + pub fn empty_with_height(height: f32) -> Self { + Self { + height, + width: 0., + max_ascent: 0., + max_descent: 0., + glyphs: Vec::new(), + } + } + + pub fn line_height(&self) -> f32 { + self.height + } + /// Calculates the line height from the lines last characters line height + pub fn last_char_line_height(&self) -> Option { + self.glyphs.iter().last().map(|g| g.line_height) + } } /// Wrapping mode diff --git a/src/lib.rs b/src/lib.rs index 52b48f6244..9ce50c3620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels. //! //! ``` -//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping}; +//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Shaping}; //! //! // A FontSystem provides access to detected system fonts, create one per application //! let mut font_system = FontSystem::new(); @@ -20,11 +20,8 @@ //! // A SwashCache stores rasterized glyphs, create one per application //! let mut swash_cache = SwashCache::new(); //! -//! // Text metrics indicate the font size and line height of a buffer -//! let metrics = Metrics::new(14.0, 20.0); -//! //! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget -//! let mut buffer = Buffer::new(&mut font_system, metrics); +//! let mut buffer = Buffer::new(&mut font_system); //! //! // Borrow buffer together with the font system for more convenient method calls //! let mut buffer = buffer.borrow_with(&mut font_system); diff --git a/src/shape.rs b/src/shape.rs index cfc9da0b24..91da5951cb 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -143,7 +143,12 @@ fn shape_fallback( glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"), //TODO: color should not be related to shaping color_opt: attrs.color_opt, + //TODO: metadata should not be related to shaping metadata: attrs.metadata, + //TODO: font_size should not be related to shaping + font_size: attrs.font_size, + //TODO: line_height should not be related to shaping + line_height: attrs.line_height.height(attrs.font_size), }); } @@ -358,8 +363,14 @@ fn shape_skip( descent, font_id, glyph_id, + //TODO: color should not be related to shaping color_opt: attrs.color_opt, + //TODO: metadata should not be related to shaping metadata: attrs.metadata, + //TODO: font_size should not be related to shaping + font_size: attrs.font_size, + //TODO: line_height should not be related to shaping + line_height: attrs.line_height.height(attrs.font_size), } }), ); @@ -378,23 +389,22 @@ pub struct ShapeGlyph { pub descent: f32, pub font_id: fontdb::ID, pub glyph_id: u16, + // TODO: extract pub color_opt: Option, + // TODO: extract pub metadata: usize, + // TODO: extract + pub font_size: f32, + // TODO: extract + pub line_height: f32, } impl ShapeGlyph { - fn layout( - &self, - font_size: f32, - x: f32, - y: f32, - w: f32, - level: unicode_bidi::Level, - ) -> LayoutGlyph { + fn layout(&self, x: f32, y: f32, w: f32, level: unicode_bidi::Level) -> LayoutGlyph { LayoutGlyph { start: self.start, end: self.end, - font_size, + font_size: self.font_size, font_id: self.font_id, glyph_id: self.glyph_id, x, @@ -403,6 +413,7 @@ impl ShapeGlyph { level, x_offset: self.x_offset, y_offset: self.y_offset, + line_height: self.line_height, color_opt: self.color_opt, metadata: self.metadata, } @@ -511,6 +522,13 @@ impl ShapeWord { y_advance, } } + + pub fn width(&self) -> f32 { + self.glyphs + .iter() + .map(|g| g.font_size * g.x_advance) + .sum::() + } } /// A shaped span (for bidirectional processing) @@ -634,7 +652,7 @@ pub struct ShapeLine { // Visual Line Ranges: (span_index, (first_word_index, first_glyph_index), (last_word_index, last_glyph_index)) type VlRange = (usize, (usize, usize), (usize, usize)); -#[derive(Default)] +#[derive(Debug, Default)] struct VisualLine { ranges: Vec, spaces: u32, @@ -850,19 +868,19 @@ impl ShapeLine { pub fn layout( &self, - font_size: f32, line_width: f32, wrap: Wrap, align: Option, + empty_height: f32, ) -> Vec { let mut lines = Vec::with_capacity(1); self.layout_to_buffer( &mut ShapeBuffer::default(), - font_size, line_width, wrap, align, &mut lines, + empty_height, ); lines } @@ -870,11 +888,12 @@ impl ShapeLine { pub fn layout_to_buffer( &self, scratch: &mut ShapeBuffer, - font_size: f32, line_width: f32, wrap: Wrap, align: Option, layout_lines: &mut Vec, + // height used to layout empty lines + empty_height: f32, ) { // For each visual line a list of (span index, and range of words in that span) // Note that a BiDi visual line could have multiple spans or parts of them @@ -913,7 +932,7 @@ impl ShapeLine { let mut word_range_width = 0.; let mut number_of_blanks: u32 = 0; for word in span.words.iter() { - let word_width = font_size * word.x_advance; + let word_width = word.width(); word_range_width += word_width; if word.blank { number_of_blanks += 1; @@ -939,7 +958,7 @@ impl ShapeLine { // incongruent directions let mut fitting_start = (span.words.len(), 0); for (i, word) in span.words.iter().enumerate().rev() { - let word_width = font_size * word.x_advance; + let word_width = word.width(); // Addition in the same order used to compute the final width, so that // relayouts with that width as the `line_width` will produce the same @@ -960,7 +979,7 @@ impl ShapeLine { continue; } else if wrap == Wrap::Glyph { for (glyph_i, glyph) in word.glyphs.iter().enumerate().rev() { - let glyph_width = font_size * glyph.x_advance; + let glyph_width = glyph.font_size * glyph.x_advance; if current_visual_line.w + (word_range_width + glyph_width) <= line_width { @@ -1043,7 +1062,7 @@ impl ShapeLine { // congruent direction let mut fitting_start = (0, 0); for (i, word) in span.words.iter().enumerate() { - let word_width = font_size * word.x_advance; + let word_width = word.width(); if current_visual_line.w + (word_range_width + word_width) <= line_width // Include one blank word over the width limit since it won't be @@ -1060,7 +1079,7 @@ impl ShapeLine { continue; } else if wrap == Wrap::Glyph { for (glyph_i, glyph) in word.glyphs.iter().enumerate() { - let glyph_width = font_size * glyph.x_advance; + let glyph_width = glyph.font_size * glyph.x_advance; if current_visual_line.w + (word_range_width + glyph_width) <= line_width { @@ -1216,7 +1235,7 @@ impl ShapeLine { (true, true) => &word.glyphs[starting_glyph..ending_glyph], }; for glyph in included_glyphs { - let x_advance = font_size * glyph.x_advance + let x_advance = glyph.font_size * glyph.x_advance + if word.blank { justification_expansion } else { @@ -1225,14 +1244,14 @@ impl ShapeLine { if self.rtl { x -= x_advance; } - let y_advance = font_size * glyph.y_advance; - glyphs.push(glyph.layout(font_size, x, y, x_advance, span.level)); + let y_advance = glyph.font_size * glyph.y_advance; + glyphs.push(glyph.layout(x, y, x_advance, span.level)); if !self.rtl { x += x_advance; } y += y_advance; - max_ascent = max_ascent.max(glyph.ascent); - max_descent = max_descent.max(glyph.descent); + max_ascent = max_ascent.max(glyph.ascent * glyph.font_size); + max_descent = max_descent.max(glyph.descent * glyph.font_size); } } } @@ -1249,28 +1268,20 @@ impl ShapeLine { } } - layout_lines.push(LayoutLine { - w: if align != Align::Justified { - visual_line.w - } else if self.rtl { - start_x - x - } else { - x - }, - max_ascent: max_ascent * font_size, - max_descent: max_descent * font_size, - glyphs, - }); + let width = if align != Align::Justified { + visual_line.w + } else if self.rtl { + start_x - x + } else { + x + }; + layout_lines.push(LayoutLine::new(width, max_ascent, max_descent, glyphs)); } - // This is used to create a visual line for empty lines (e.g. lines with only a ) + // This is used to create a visual line for empty lines e.g. lines with only a `\n` + // as the source of its existance if layout_lines.is_empty() { - layout_lines.push(LayoutLine { - w: 0.0, - max_ascent: 0.0, - max_descent: 0.0, - glyphs: Default::default(), - }); + layout_lines.push(LayoutLine::empty_with_height(empty_height)); } // Restore the buffer to the scratch set to prevent reallocations. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9e250f84fb..c4f86cd98b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,8 +1,7 @@ use std::path::PathBuf; use cosmic_text::{ - fontdb::Database, Attrs, AttrsOwned, Buffer, Color, Family, FontSystem, Metrics, Shaping, - SwashCache, + fontdb::Database, Attrs, AttrsOwned, Buffer, Color, Family, FontSystem, Shaping, SwashCache, }; use tiny_skia::{Paint, Pixmap, Rect, Transform}; @@ -84,15 +83,21 @@ impl DrawTestCfg { font_db.load_fonts_dir(fonts_path); let mut font_system = FontSystem::new_with_locale_and_db("En-US".into(), font_db); let mut swash_cache = SwashCache::new(); - let metrics = Metrics::new(self.font_size, self.line_height); - let mut buffer = Buffer::new(&mut font_system, metrics); + let mut buffer = Buffer::new(&mut font_system); let mut buffer = buffer.borrow_with(&mut font_system); let margins = 5; buffer.set_size( (self.canvas_width - margins * 2) as f32, (self.canvas_height - margins * 2) as f32, ); - buffer.set_text(&self.text, self.font.as_attrs(), Shaping::Advanced); + buffer.set_text( + &self.text, + self.font + .as_attrs() + .font_size(self.font_size) + .line_height(cosmic_text::LineHeight::Absolute(self.line_height)), + Shaping::Advanced, + ); buffer.shape_until_scroll(); // Black diff --git a/tests/wrap_stability.rs b/tests/wrap_stability.rs index 7bce4d4574..64f8954659 100644 --- a/tests/wrap_stability.rs +++ b/tests/wrap_stability.rs @@ -13,7 +13,8 @@ fn stable_wrap() { let attrs = AttrsList::new( Attrs::new() .family(Family::Name("FiraMono")) - .weight(Weight::MEDIUM), + .weight(Weight::MEDIUM) + .font_size(font_size), ); let mut font_system = FontSystem::new_with_locale_and_db("en-US".into(), fontdb::Database::new()); @@ -23,12 +24,12 @@ fn stable_wrap() { let mut check_wrap = |text: &_, wrap, start_width| { let line = ShapeLine::new(&mut font_system, text, &attrs, Shaping::Advanced); - let layout_unbounded = line.layout(font_size, start_width, wrap, Some(Align::Left)); - let max_width = layout_unbounded.iter().map(|l| l.w).fold(0.0, f32::max); + let layout_unbounded = line.layout(start_width, wrap, Some(Align::Left), 20.0); + let max_width = layout_unbounded.iter().map(|l| l.width).fold(0.0, f32::max); let new_limit = f32::min(start_width, max_width); - let layout_bounded = line.layout(font_size, new_limit, wrap, Some(Align::Left)); - let bounded_max_width = layout_bounded.iter().map(|l| l.w).fold(0.0, f32::max); + let layout_bounded = line.layout(new_limit, wrap, Some(Align::Left), 20.0); + let bounded_max_width = layout_bounded.iter().map(|l| l.width).fold(0.0, f32::max); // For debugging: // dbg_layout_lines(text, &layout_unbounded); @@ -40,7 +41,7 @@ fn stable_wrap() { "Wrap \"{wrap:?}\" with text: \"{text}\"", ); for (u, b) in layout_unbounded[1..].iter().zip(layout_bounded[1..].iter()) { - assert_eq!(u.w, b.w, "Wrap {wrap:?} with text: \"{text}\"",); + assert_eq!(u.width, b.width, "Wrap {wrap:?} with text: \"{text}\"",); } };