From 68b51cdde0db684104ac4920f65a52cf0178fd7f Mon Sep 17 00:00:00 2001 From: tigregalis Date: Wed, 28 Jun 2023 21:55:30 +0800 Subject: [PATCH 01/36] move `font_size` into `Attrs`, adjust layout --- src/attrs.rs | 49 +++++++++++++++++++++++++++++++++++-- src/buffer.rs | 22 +++++------------ src/buffer_line.rs | 6 ++--- src/shape.rs | 60 +++++++++++++++++++++++----------------------- 4 files changed, 85 insertions(+), 52 deletions(-) diff --git a/src/attrs.rs b/src/attrs.rs index e163124a24..8c00ad8f89 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -100,15 +100,19 @@ impl FamilyOwned { } /// 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, } impl<'a> Attrs<'a> { @@ -122,6 +126,7 @@ impl<'a> Attrs<'a> { stretch: Stretch::Normal, style: Style::Normal, weight: Weight::NORMAL, + font_size: 16.0, metadata: 0, } } @@ -132,6 +137,12 @@ impl<'a> Attrs<'a> { self } + /// Set font size + pub fn size(mut self, size: f32) -> Self { + self.font_size = size; + self + } + /// Set [Family] pub fn family(mut self, family: Family<'a>) -> Self { self.family = family; @@ -180,16 +191,34 @@ impl<'a> Attrs<'a> { } } +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); + } +} + /// 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, } impl AttrsOwned { @@ -201,6 +230,7 @@ impl AttrsOwned { style: attrs.style, weight: attrs.weight, metadata: attrs.metadata, + font_size: attrs.font_size, } } @@ -212,10 +242,25 @@ impl AttrsOwned { style: self.style, weight: self.weight, metadata: self.metadata, + font_size: self.font_size, } } } +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); + } +} + /// 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 a04675dd1c..829070a4c1 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -390,7 +390,7 @@ 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); } } @@ -415,13 +415,8 @@ 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, - ); + let layout = + line.layout_in_buffer(&mut self.scratch, font_system, self.width, self.wrap); total_layout += layout.len() as i32; } @@ -449,13 +444,8 @@ 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, - ); + 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; @@ -538,7 +528,7 @@ impl Buffer { line_i: usize, ) -> Option<&[LayoutLine]> { let line = self.lines.get_mut(line_i)?; - Some(line.layout(font_system, self.metrics.font_size, self.width, self.wrap)) + Some(line.layout(font_system, self.width, self.wrap)) } /// Get the current [`Metrics`] diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 667cc80cd6..8ddcdd736f 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -200,7 +200,6 @@ impl BufferLine { pub fn layout( &mut self, font_system: &mut FontSystem, - font_size: f32, width: f32, wrap: Wrap, ) -> &[LayoutLine] { @@ -208,7 +207,7 @@ impl BufferLine { self.wrap = wrap; let align = self.align; let shape = self.shape(font_system); - let layout = shape.layout(font_size, width, wrap, align); + let layout = shape.layout(width, wrap, align); self.layout_opt = Some(layout); } self.layout_opt.as_ref().expect("layout not found") @@ -219,7 +218,6 @@ impl BufferLine { &mut self, scratch: &mut ShapeBuffer, font_system: &mut FontSystem, - font_size: f32, width: f32, wrap: Wrap, ) -> &[LayoutLine] { @@ -228,7 +226,7 @@ impl BufferLine { 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); + shape.layout_to_buffer(scratch, width, wrap, align, &mut layout); self.layout_opt = Some(layout); } self.layout_opt.as_ref().expect("layout not found") diff --git a/src/shape.rs b/src/shape.rs index 53cba574a3..5dc207a67a 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -143,7 +143,10 @@ 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, }); } @@ -359,6 +362,7 @@ fn shape_skip( glyph_id, color_opt: attrs.color_opt, metadata: attrs.metadata, + font_size: attrs.font_size, } }), ); @@ -377,23 +381,20 @@ 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, } 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, @@ -510,6 +511,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) @@ -847,17 +855,10 @@ impl ShapeLine { runs } - pub fn layout( - &self, - font_size: f32, - line_width: f32, - wrap: Wrap, - align: Option, - ) -> Vec { + pub fn layout(&self, line_width: f32, wrap: Wrap, align: Option) -> Vec { let mut lines = Vec::with_capacity(1); self.layout_to_buffer( &mut ShapeBuffer::default(), - font_size, line_width, wrap, align, @@ -869,7 +870,6 @@ impl ShapeLine { pub fn layout_to_buffer( &self, scratch: &mut ShapeBuffer, - font_size: f32, line_width: f32, wrap: Wrap, align: Option, @@ -912,7 +912,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; @@ -938,7 +938,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 @@ -959,7 +959,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 { @@ -1042,7 +1042,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 @@ -1059,7 +1059,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 { @@ -1215,7 +1215,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 { @@ -1224,14 +1224,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); } } } @@ -1258,8 +1258,8 @@ impl ShapeLine { x } }, - max_ascent: max_ascent * font_size, - max_descent: max_descent * font_size, + max_ascent, + max_descent, glyphs, }); } From a3a91eaf5315504e0071c1946e60fd58096e2105 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 30 Jun 2023 21:35:25 +0800 Subject: [PATCH 02/36] temporarily update rich-text example --- examples/rich-text/src/main.rs | 54 ++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 66092db98b..dae0bb6afb 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -46,8 +46,9 @@ fn main() { .buffer_mut() .set_size(window.width() as f32, window.height() as f32); - let attrs = Attrs::new(); - let serif_attrs = attrs.family(Family::Serif); + let font_size = editor.buffer().metrics().font_size; + let attrs = Attrs::new().size(font_size); + let serif_attrs = attrs.family(Family::Serif).size(attrs.font_size * 1.5); let mono_attrs = attrs.family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); @@ -97,13 +98,48 @@ 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)) + .size(attrs.font_size * 1.9), + ), + ( + "Orange ", + attrs + .color(Color::rgb(0xFF, 0x7F, 0x00)) + .size(attrs.font_size * 1.6), + ), + ( + "Yellow ", + attrs + .color(Color::rgb(0xFF, 0xFF, 0x00)) + .size(attrs.font_size * 1.3), + ), + ( + "Green ", + attrs + .color(Color::rgb(0x00, 0xFF, 0x00)) + .size(attrs.font_size * 1.0), + ), + ( + "Blue ", + attrs + .color(Color::rgb(0x00, 0x00, 0xFF)) + .size(attrs.font_size * 0.8), + ), + ( + "Indigo ", + attrs + .color(Color::rgb(0x4B, 0x00, 0x82)) + .size(attrs.font_size * 0.6), + ), + ( + "Violet ", + attrs + .color(Color::rgb(0x94, 0x00, 0xD3)) + .size(attrs.font_size * 0.4), + ), ("U", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), ("N", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), ("I", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), From 6af188e280d95a27b44808f5af981ae2545a581a Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 30 Jun 2023 21:36:09 +0800 Subject: [PATCH 03/36] update layout runs with line height --- src/buffer.rs | 48 ++++++++++++++++++++++++++---------------------- src/layout.rs | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 829070a4c1..f1adfd2e9d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -193,27 +193,30 @@ pub struct LayoutRunIter<'b> { layout_i: usize, remaining_len: usize, total_layout: i32, + line_heights: Vec, } impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { - let total_layout_lines: usize = buffer + let line_heights = buffer .lines .iter() - .map(|line| { - line.layout_opt() - .as_ref() - .map(|layout| layout.len()) - .unwrap_or_default() - }) - .sum(); + .flat_map(|line| line.layout_opt()) + .flat_map(|lines| lines.iter().map(|line| line.line_height())) + .collect::>(); + 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 - } else { - (buffer.height / buffer.metrics.line_height) as i32 - }; + let mut maximum_lines = line_heights.len() as i32; + let mut remaining_height = buffer.height; + for (line_number, line_height) in line_heights.iter().enumerate() { + remaining_height -= line_height; + if remaining_height < 0.0 { + maximum_lines = line_number as i32; + break; + } + } + let maximum_lines = maximum_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() @@ -227,6 +230,7 @@ impl<'b> LayoutRunIter<'b> { layout_i: 0, remaining_len: bottom_cropped_layout_lines, total_layout: 0, + line_heights, } } } @@ -250,14 +254,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 as usize + self.buffer.scroll as usize - 1; + let line_top = self.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.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 { @@ -297,10 +300,11 @@ pub struct Metrics { } impl Metrics { - pub const fn new(font_size: f32, line_height: f32) -> Self { + pub fn new(font_size: f32, line_height: f32) -> Self { Self { font_size, - line_height, + // TODO: remove this (not hardcoded) + line_height: font_size * 1.2, } } diff --git a/src/layout.rs b/src/layout.rs index 66513ae3f3..62ac995c55 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -63,6 +63,11 @@ pub struct PhysicalGlyph { } impl LayoutGlyph { + pub fn line_height(&self) -> f32 { + // TODO: should not be hardcoded / should come from Attrs + self.font_size * 1.2 + } + pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph { let x_offset = self.font_size * self.x_offset; let y_offset = self.font_size * self.y_offset; @@ -94,6 +99,16 @@ pub struct LayoutLine { pub glyphs: Vec, } +impl LayoutLine { + pub fn line_height(&self) -> f32 { + self.glyphs + .iter() + .map(|g| g.line_height()) + .reduce(f32::max) + .unwrap_or_default() + } +} + /// Wrapping mode #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum Wrap { From fb7c90758cf73e5d3b3bb3e50c0d232e951d6e49 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 4 Jul 2023 20:25:07 +0800 Subject: [PATCH 04/36] WIP implement hit detection --- examples/rich-text/src/main.rs | 2 +- src/buffer.rs | 291 ++++++++++++++++++++++----------- 2 files changed, 195 insertions(+), 98 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index dae0bb6afb..be0cb97fb8 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -46,7 +46,7 @@ fn main() { .buffer_mut() .set_size(window.width() as f32, window.height() as f32); - let font_size = editor.buffer().metrics().font_size; + let font_size = editor.buffer().metrics().font_size_; let attrs = Attrs::new().size(font_size); let serif_attrs = attrs.family(Family::Serif).size(attrs.font_size * 1.5); let mono_attrs = attrs.family(Family::Monospace); diff --git a/src/buffer.rs b/src/buffer.rs index f1adfd2e9d..091106bfd9 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -183,6 +183,15 @@ impl<'a> LayoutRun<'a> { Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) } } + + pub fn line_height(&self) -> f32 { + self.glyphs + .iter() + .map(|g| g.line_height()) + .reduce(f32::max) + // TODO: not 0 + .unwrap_or(0.0) + } } /// An iterator of visible text lines, see [`LayoutRun`] @@ -193,17 +202,13 @@ pub struct LayoutRunIter<'b> { layout_i: usize, remaining_len: usize, total_layout: i32, + // TODO: lift this to BufferLine::layout_opt, and take a &'b [f32] slice instead line_heights: Vec, } impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { - let line_heights = buffer - .lines - .iter() - .flat_map(|line| line.layout_opt()) - .flat_map(|lines| lines.iter().map(|line| line.line_height())) - .collect::>(); + 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()); @@ -216,7 +221,6 @@ impl<'b> LayoutRunIter<'b> { break; } } - let maximum_lines = maximum_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() @@ -294,31 +298,31 @@ impl<'b> ExactSizeIterator for LayoutRunIter<'b> {} #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Metrics { /// Font size in pixels - pub font_size: f32, + pub font_size_: f32, /// Line height in pixels - pub line_height: f32, + pub line_height_: f32, } impl Metrics { pub fn new(font_size: f32, line_height: f32) -> Self { Self { - font_size, + font_size_: font_size, // TODO: remove this (not hardcoded) - line_height: font_size * 1.2, + line_height_: font_size * 1.2, } } pub fn scale(self, scale: f32) -> Self { Self { - font_size: self.font_size * scale, - line_height: self.line_height * scale, + 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) + write!(f, "{}px / {}px", self.font_size_, self.line_height_) } } @@ -352,7 +356,7 @@ impl Buffer { /// /// 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"); + assert_ne!(metrics.line_height_, 0.0, "line height cannot be 0"); Self { lines: Vec::new(), metrics, @@ -404,6 +408,14 @@ impl Buffer { log::debug!("relayout: {:?}", instant.elapsed()); } + pub fn line_heights(&self) -> Vec { + self.lines + .iter() + .flat_map(|line| line.layout_opt()) + .flat_map(|lines| lines.iter().map(|line| line.line_height())) + .collect::>() + } + /// 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")))] @@ -547,7 +559,7 @@ impl Buffer { /// Will panic if `metrics.font_size` is zero. pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { if metrics != self.metrics { - assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); + assert_ne!(metrics.font_size_, 0.0, "font size cannot be 0"); self.metrics = metrics; self.relayout(font_system); self.shape_until_scroll(font_system); @@ -601,7 +613,8 @@ impl Buffer { /// Get the number of lines that can be viewed in the buffer pub fn visible_lines(&self) -> i32 { - (self.height / self.metrics.line_height) as i32 + // TODO + 1000_000 } /// Set text of buffer, using provided attributes for each line by default @@ -733,104 +746,188 @@ 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; - } + println!("strike_y: {strike_y}"); + + // 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 last_run_index = self.layout_runs().count() - 1; + + let mut runs = self.layout_runs().enumerate(); + + // TODO: cache line_top, run.line_height(), 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)), + } + } + + println!("{run_index}: [{line_top} .. {line_bot}]"); + + if (line_top..line_bot).contains(&strike_y) { + println!( + "-> hit on line {run_index} \"{run_text}\"", + run_text = run.text + ); + + 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) + }; + + // TODO: check Arabic + 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)); } - if x >= glyph.x && x <= glyph.x + glyph.w { - new_cursor_glyph = glyph_i; + 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)); + } + + // let glyph_mid = glyph_left + glyph.w / 2.0; + + // println!("{glyph_index}: [{glyph_left} .. {glyph_mid} .. {glyph_right}]"); + + // let hit_glyph = if (glyph_left..glyph_mid).contains(&strike_x) { + // // hit left half of glyph + // println!("-> hit on glyph {glyph_index} (left half)"); + // Some(true) + // } else if (glyph_mid..glyph_right).contains(&strike_x) { + // // hit right half of glyph + // println!("-> hit on glyph {glyph_index} (right half)"); + // Some(false) + // } else { + // None + // }; + + // if let Some(glyph_left_half) = hit_glyph { + 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; + println!( + "--> hit on glyph with value \"{cluster}\" [{len}]", + len = cluster.len() + ); + + 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; + dbg!(egc_w, total); + 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(); + if egc_start != 0 { + dbg!(egc_start); } - 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 (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) + }; - let mut new_cursor = Cursor::new(run.line_i, 0); + if egc_index == left_egc_index && strike_x < egc_left { + // hit left of left-most egc in cluster + println!("-> hit left of left-most egc in cluster"); + break 'hit Some(Cursor::new(run.line_i, left_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_right = egc_left + egc_w; + + if egc_index == right_egc_index && strike_x >= egc_right { + // hit right of right-most egc in cluster + println!("-> hit right of right-most egc in cluster"); + break 'hit Some(Cursor::new(run.line_i, right_egc_byte)); + } - new_cursor_opt = Some(new_cursor); + let egc_mid = egc_left + egc_w / 2.0; + + println!("{egc_index}: [{egc_left} .. {egc_mid} .. {egc_right}]"); + + let hit_egc = if (egc_left..egc_mid).contains(&strike_x) { + // hit left half of egc + println!("-> hit on egc {egc_index} (left half)"); + Some(true) + } else if (egc_mid..egc_right).contains(&strike_x) { + // hit right half of egc + println!("-> hit on egc {egc_index} (right half)"); + Some(false) + } else { + None + }; + + if let Some(egc_left_half) = hit_egc { + println!("--> hit on egc with value \"{egc}\""); + break 'hit Some(Cursor::new( + run.line_i, + if egc_left_half { + left_egc_byte + } else { + right_egc_byte + }, + )); + } - 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); + egc_left = egc_right; + } + } } - 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 From 9a8c53352291a1ac2d3c830cfe41b8f3c8ec654d Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 4 Jul 2023 20:25:46 +0800 Subject: [PATCH 05/36] WIP update editor --- src/edit/editor.rs | 55 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 274e05bd49..93b34bbf65 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -448,22 +448,56 @@ 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) { - Ordering::Less => { - for _ in 0..-lines { + let line_heights = self.buffer.line_heights(); + dbg!(line_heights.len()); + 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 >= line_heights.len() as i32 { + break; + } + + let current_line_height = 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; + } } - } - 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, } - Ordering::Equal => {} } + dbg!(current_line, self.buffer.scroll()); + // 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 => { if self.select_opt.take().is_some() { @@ -700,12 +734,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 { From 346564e0ffff24e77a02abc82a06680c288ac5e2 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 24 Jul 2023 07:49:54 +0800 Subject: [PATCH 06/36] fix scrolling partially --- src/buffer.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 091106bfd9..d8bff7db61 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -201,6 +201,7 @@ pub struct LayoutRunIter<'b> { line_i: usize, layout_i: usize, remaining_len: usize, + total_layout_height: f32, total_layout: i32, // TODO: lift this to BufferLine::layout_opt, and take a &'b [f32] slice instead line_heights: Vec, @@ -208,6 +209,7 @@ pub struct LayoutRunIter<'b> { impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { + dbg!(buffer.scroll); let line_heights = buffer.line_heights(); let total_layout_lines = line_heights.len(); let top_cropped_layout_lines = @@ -227,12 +229,12 @@ impl<'b> LayoutRunIter<'b> { } else { top_cropped_layout_lines }; - Self { buffer, line_i: 0, layout_i: 0, remaining_len: bottom_cropped_layout_lines, + total_layout_height: 0.0, total_layout: 0, line_heights, } @@ -259,10 +261,12 @@ impl<'b> Iterator for LayoutRunIter<'b> { continue; } // TODO: can scroll be negative? - let this_line = self.total_layout as usize + self.buffer.scroll as usize - 1; + // let this_line = self.total_layout as usize + self.buffer.scroll as usize - 1; + let this_line = self.total_layout.saturating_sub(1) as usize; let line_top = self.line_heights[self.buffer.scroll as usize..this_line] .iter() .sum(); + // dbg!(line_top); let glyph_height = layout_line.max_ascent + layout_line.max_descent; let centering_offset = (self.line_heights[this_line] - glyph_height) / 2.0; let line_y = line_top + centering_offset + layout_line.max_ascent; @@ -489,12 +493,18 @@ impl Buffer { /// Shape lines until scroll pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem) { + println!( + "{:?} ======================", + std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) + ); + println!("visible_lines() called from shape_until_scroll"); let lines = self.visible_lines(); let scroll_end = self.scroll + lines; + dbg!(self.scroll, lines, scroll_end); let total_layout = self.shape_until(font_system, scroll_end); - - self.scroll = cmp::max(0, cmp::min(total_layout - (lines - 1), self.scroll)); + dbg!(total_layout); + self.scroll = (total_layout - (lines - 1)).clamp(0, self.scroll); } pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor { @@ -613,8 +623,23 @@ impl Buffer { /// Get the number of lines that can be viewed in the buffer pub fn visible_lines(&self) -> i32 { - // TODO - 1000_000 + 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 + return i32::MAX; + } + let mut total_height = 0.0; + for (i, line_height) in line_heights.iter().skip(self.scroll as usize).enumerate() { + println!("{i}: line_height({line_height})"); + total_height += line_height; + height -= line_height; + if height <= 0.0 { + dbg!(self.scroll, i, total_height); + return i as i32; + } + } + 1 } /// Set text of buffer, using provided attributes for each line by default From 7179d3e10585872276f5e32acb0c4e990ac82645 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 24 Jul 2023 11:15:44 +0800 Subject: [PATCH 07/36] fix scrolling --- src/buffer.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index d8bff7db61..6567908f13 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -626,20 +626,19 @@ impl Buffer { 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 + // this has never been laid out, so we can't know the height yet return i32::MAX; } - let mut total_height = 0.0; - for (i, line_height) in line_heights.iter().skip(self.scroll as usize).enumerate() { - println!("{i}: line_height({line_height})"); - total_height += line_height; + let mut i = 0; + let mut iter = line_heights.iter().skip(self.scroll as usize); + while let Some(line_height) = iter.next() { height -= line_height; if height <= 0.0 { - dbg!(self.scroll, i, total_height); - return i as i32; + break; } + i += 1; } - 1 + i } /// Set text of buffer, using provided attributes for each line by default From d5940e30eff8d3c7fb3392c499b03bdd2626200b Mon Sep 17 00:00:00 2001 From: tigregalis Date: Thu, 27 Jul 2023 22:02:14 +0800 Subject: [PATCH 08/36] fix max lines and scroll into view --- src/buffer.rs | 138 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 40 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 6567908f13..094c1187ce 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -209,26 +209,16 @@ pub struct LayoutRunIter<'b> { impl<'b> LayoutRunIter<'b> { pub fn new(buffer: &'b Buffer) -> Self { - dbg!(buffer.scroll); 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 mut maximum_lines = line_heights.len() as i32; - let mut remaining_height = buffer.height; - for (line_number, line_height) in line_heights.iter().enumerate() { - remaining_height -= line_height; - if remaining_height < 0.0 { - maximum_lines = line_number as i32; - break; - } - } - 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 - }; + 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 { + top_cropped_layout_lines + }; Self { buffer, line_i: 0, @@ -330,6 +320,14 @@ impl fmt::Display for Metrics { } } +macro_rules! dbgg { + ($ex:expr) => { + if cfg!(debug_assertions) { + println!("{}: {}", stringify!($ex), $ex); + } + }; +} + /// A buffer of text that is shaped and laid out #[derive(Debug)] pub struct Buffer { @@ -427,7 +425,8 @@ impl Buffer { let mut reshaped = 0; let mut total_layout = 0; - for line in &mut self.lines { + dbgg!(lines); + for (index, line) in &mut self.lines.iter_mut().enumerate() { if total_layout >= lines { break; } @@ -451,6 +450,7 @@ impl Buffer { /// Shape lines until cursor, also scrolling to include cursor in view pub fn shape_until_cursor(&mut self, font_system: &mut FontSystem, cursor: Cursor) { + println!("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); @@ -481,10 +481,26 @@ impl Buffer { self.redraw = true; } + // the first visible line is index = self.scroll + // the last visible line is index = self.scroll + lines + for (last_line, _) in self + .lines + .iter() + .filter_map(|line| line.layout_opt().as_ref()) + .flat_map(|para| para.iter()) + .enumerate() + .skip(self.scroll as usize) + .take(self.visible_lines() as usize) + { + dbg!(last_line); + } let lines = self.visible_lines(); + dbg!(layout_i, self.scroll, 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); } @@ -621,8 +637,8 @@ impl Buffer { } } - /// Get the number of lines that can be viewed in the buffer - pub fn visible_lines(&self) -> i32 { + /// 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() { @@ -630,7 +646,7 @@ impl Buffer { return i32::MAX; } let mut i = 0; - let mut iter = line_heights.iter().skip(self.scroll as usize); + let mut iter = line_heights.iter().skip(from); while let Some(line_height) = iter.next() { height -= line_height; if height <= 0.0 { @@ -641,6 +657,48 @@ impl Buffer { 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 { + println!("called visible lines to"); + 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 lines that can be viewed in the buffer + pub fn visible_lines(&self) -> i32 { + self.visible_lines_from(self.scroll as usize) + // 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(self.scroll as usize); + // while let Some(line_height) = iter.next() { + // height -= line_height; + // if height <= 0.0 { + // break; + // } + // i += 1; + // } + // i + } + /// Set text of buffer, using provided attributes for each line by default pub fn set_text( &mut self, @@ -774,7 +832,7 @@ impl Buffer { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); - println!("strike_y: {strike_y}"); + // println!("strike_y: {strike_y}"); // below, // - `first`, `last` refers to iterator indices (usize) @@ -813,13 +871,13 @@ impl Buffer { } } - println!("{run_index}: [{line_top} .. {line_bot}]"); + // println!("{run_index}: [{line_top} .. {line_bot}]"); if (line_top..line_bot).contains(&strike_y) { - println!( - "-> hit on line {run_index} \"{run_text}\"", - run_text = run.text - ); + // println!( + // "-> hit on line {run_index} \"{run_text}\"", + // run_text = run.text + // ); let last_glyph_index = run.glyphs.len() - 1; @@ -865,15 +923,15 @@ impl Buffer { // if let Some(glyph_left_half) = hit_glyph { if (glyph_left..glyph_right).contains(&strike_x) { let cluster = &run.text[glyph.start..glyph.end]; - println!( - "--> hit on glyph with value \"{cluster}\" [{len}]", - len = cluster.len() - ); + // println!( + // "--> hit on glyph with value \"{cluster}\" [{len}]", + // len = cluster.len() + // ); let total = cluster.graphemes(true).count(); let last_egc_index = total - 1; let egc_w = glyph.w / (total as f32); - dbg!(egc_w, total); + // dbg!(egc_w, total); let mut egc_left = glyph_left; // TODO: is this assumption correct with rtl? @@ -887,9 +945,9 @@ impl Buffer { cluster.grapheme_indices(true).enumerate() { let egc_end = egc_start + egc.len(); - if egc_start != 0 { - dbg!(egc_start); - } + // if egc_start != 0 { + // dbg!(egc_start); + // } let (left_egc_byte, right_egc_byte) = if glyph.level.is_rtl() { (glyph.start + egc_end, glyph.start + egc_start) @@ -899,7 +957,7 @@ impl Buffer { if egc_index == left_egc_index && strike_x < egc_left { // hit left of left-most egc in cluster - println!("-> hit left of left-most egc in cluster"); + // println!("-> hit left of left-most egc in cluster"); break 'hit Some(Cursor::new(run.line_i, left_egc_byte)); } @@ -907,28 +965,28 @@ impl Buffer { if egc_index == right_egc_index && strike_x >= egc_right { // hit right of right-most egc in cluster - println!("-> hit right of right-most egc in cluster"); + // println!("-> hit right of right-most egc in cluster"); break 'hit Some(Cursor::new(run.line_i, right_egc_byte)); } let egc_mid = egc_left + egc_w / 2.0; - println!("{egc_index}: [{egc_left} .. {egc_mid} .. {egc_right}]"); + // println!("{egc_index}: [{egc_left} .. {egc_mid} .. {egc_right}]"); let hit_egc = if (egc_left..egc_mid).contains(&strike_x) { // hit left half of egc - println!("-> hit on egc {egc_index} (left half)"); + // println!("-> hit on egc {egc_index} (left half)"); Some(true) } else if (egc_mid..egc_right).contains(&strike_x) { // hit right half of egc - println!("-> hit on egc {egc_index} (right half)"); + // println!("-> hit on egc {egc_index} (right half)"); Some(false) } else { None }; if let Some(egc_left_half) = hit_egc { - println!("--> hit on egc with value \"{egc}\""); + // println!("--> hit on egc with value \"{egc}\""); break 'hit Some(Cursor::new( run.line_i, if egc_left_half { From bd16490bc17d7674a0e27ef22b05f7c546476b40 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 28 Jul 2023 22:34:12 +0800 Subject: [PATCH 09/36] cache line heights --- src/buffer.rs | 131 ++++++++++++++++++++++++++++++++++++++------- src/buffer_line.rs | 14 +++++ src/edit/editor.rs | 8 ++- 3 files changed, 130 insertions(+), 23 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 094c1187ce..35e8af99eb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -127,6 +127,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> { @@ -184,14 +186,14 @@ impl<'a> LayoutRun<'a> { } } - pub fn line_height(&self) -> f32 { - self.glyphs - .iter() - .map(|g| g.line_height()) - .reduce(f32::max) - // TODO: not 0 - .unwrap_or(0.0) - } + // pub fn line_height(&self) -> f32 { + // self.glyphs + // .iter() + // .map(|g| g.line_height()) + // .reduce(f32::max) + // // TODO: not 0 + // .unwrap_or(0.0) + // } } /// An iterator of visible text lines, see [`LayoutRun`] @@ -204,7 +206,7 @@ pub struct LayoutRunIter<'b> { total_layout_height: f32, total_layout: i32, // TODO: lift this to BufferLine::layout_opt, and take a &'b [f32] slice instead - line_heights: Vec, + // line_heights: &'b [f32], } impl<'b> LayoutRunIter<'b> { @@ -226,7 +228,7 @@ impl<'b> LayoutRunIter<'b> { remaining_len: bottom_cropped_layout_lines, total_layout_height: 0.0, total_layout: 0, - line_heights, + // line_heights, } } } @@ -253,12 +255,12 @@ impl<'b> Iterator for LayoutRunIter<'b> { // TODO: can scroll be negative? // let this_line = self.total_layout as usize + self.buffer.scroll as usize - 1; let this_line = self.total_layout.saturating_sub(1) as usize; - let line_top = self.line_heights[self.buffer.scroll as usize..this_line] + let line_top = self.buffer.line_heights[self.buffer.scroll as usize..this_line] .iter() .sum(); // dbg!(line_top); let glyph_height = layout_line.max_ascent + layout_line.max_descent; - let centering_offset = (self.line_heights[this_line] - 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 { @@ -275,6 +277,7 @@ impl<'b> Iterator for LayoutRunIter<'b> { line_y, line_top, line_w: layout_line.w, + line_height: self.buffer.line_heights[this_line], } }); } @@ -333,6 +336,7 @@ macro_rules! dbgg { pub struct Buffer { /// [BufferLine]s (or paragraphs) of text in the buffer pub lines: Vec, + line_heights: Vec, metrics: Metrics, width: f32, height: f32, @@ -361,6 +365,7 @@ impl Buffer { assert_ne!(metrics.line_height_, 0.0, "line height cannot be 0"); Self { lines: Vec::new(), + line_heights: Vec::new(), metrics, width: 0.0, height: 0.0, @@ -404,18 +409,46 @@ impl Buffer { } } + self.update_line_heights(); self.redraw = true; #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!("relayout: {:?}", instant.elapsed()); } - pub fn line_heights(&self) -> Vec { - self.lines + pub fn line_heights(&self) -> &[f32] { + // self.lines + // .iter() + // .flat_map(|line| line.layout_opt()) + // .flat_map(|lines| lines.iter().map(|line| line.line_height())) + // .collect() + // self.lines + // .iter() + // .flat_map(|line| line.line_heights()) + // .flat_map(|lines| lines.iter().copied()) + // .collect() + self.line_heights.as_slice() + } + + 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.layout_opt()) - .flat_map(|lines| lines.iter().map(|line| line.line_height())) - .collect::>() + .flat_map(|line| line.line_heights()) + .flat_map(|lines| lines.iter().copied()); + self.line_heights.extend(iter); + dbg!(&self.line_heights); + + #[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 @@ -425,6 +458,7 @@ impl Buffer { let mut reshaped = 0; let mut total_layout = 0; + let mut should_update_line_heights = false; dbgg!(lines); for (index, line) in &mut self.lines.iter_mut().enumerate() { if total_layout >= lines { @@ -434,11 +468,26 @@ impl Buffer { if line.shape_opt().is_none() { reshaped += 1; } + + 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); + // dbg!( + // index, + // total_layout, + // layout.len(), + // total_layout + layout.len() as i32 + // ); 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()); @@ -456,6 +505,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; @@ -464,6 +514,11 @@ impl Buffer { if line.shape_opt().is_none() { reshaped += 1; } + + 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 { @@ -475,6 +530,10 @@ 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()); @@ -514,6 +573,9 @@ impl Buffer { std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) ); println!("visible_lines() called from shape_until_scroll"); + + self.layout_lines(font_system); + let lines = self.visible_lines(); let scroll_end = self.scroll + lines; @@ -569,10 +631,43 @@ 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)?; + + // return cached layout Some(line.layout(font_system, self.width, self.wrap)) } + /// 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 [`Metrics`] pub fn metrics(&self) -> Metrics { self.metrics @@ -861,7 +956,7 @@ impl Buffer { break 'hit Some(Cursor::new(run.line_i, 0)); } - let line_bot = line_top + run.line_height(); + let line_bot = line_top + run.line_height; if run_index == last_run_index && strike_y >= line_bot { // hit below bottom line diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 8ddcdd736f..e5dd9b09e5 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -13,6 +13,7 @@ pub struct BufferLine { align: Option, shape_opt: Option, layout_opt: Option>, + line_heights: Option>, shaping: Shaping, } @@ -28,6 +29,7 @@ impl BufferLine { align: None, shape_opt: None, layout_opt: None, + line_heights: None, shaping, } } @@ -155,11 +157,13 @@ impl BufferLine { pub fn reset(&mut self) { self.shape_opt = None; self.layout_opt = None; + self.line_heights = None; } /// Reset only layout information pub fn reset_layout(&mut self) { self.layout_opt = None; + self.line_heights = None; } /// Check if shaping and layout information is cleared @@ -187,6 +191,7 @@ impl BufferLine { self.shaping, )); self.layout_opt = None; + self.line_heights = None; } self.shape_opt.as_ref().expect("shape not found") } @@ -197,6 +202,8 @@ 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, @@ -208,7 +215,9 @@ impl BufferLine { let align = self.align; let shape = self.shape(font_system); let layout = shape.layout(width, wrap, align); + let line_heights = layout.iter().map(|line| line.line_height()).collect(); self.layout_opt = Some(layout); + self.line_heights = Some(line_heights); } self.layout_opt.as_ref().expect("layout not found") } @@ -236,4 +245,9 @@ impl BufferLine { pub fn layout_opt(&self) -> &Option> { &self.layout_opt } + + /// Get line height cache, maps from [`Self::layout_opt`] + pub fn line_heights(&self) -> &Option> { + &self.line_heights + } } diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 93b34bbf65..a08601c58d 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -450,18 +450,16 @@ impl Edit for Editor { } Action::Vertical(mut px) => { // TODO more efficient - let line_heights = self.buffer.line_heights(); - dbg!(line_heights.len()); 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 >= line_heights.len() as i32 { + if current_line < 0 || current_line >= self.buffer.line_heights().len() as i32 { break; } - let current_line_height = line_heights[current_line as usize]; + let current_line_height = self.buffer.line_heights()[current_line as usize]; match direction { -1 => { @@ -738,7 +736,7 @@ impl Edit for Editor { 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 line_height = run.line_height; let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> { if cursor.line == line_i { From 44b60433de06830175339f29b2943e6162873ce7 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 15:39:02 +0800 Subject: [PATCH 10/36] remove debugging, dead code --- src/buffer.rs | 126 +-------------------------------------------- src/edit/editor.rs | 6 +-- 2 files changed, 3 insertions(+), 129 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 35e8af99eb..04033c862b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -5,7 +5,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::{cmp, fmt}; +use core::fmt; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -185,15 +185,6 @@ impl<'a> LayoutRun<'a> { Cursor::new_with_affinity(self.line_i, glyph.end, Affinity::Before) } } - - // pub fn line_height(&self) -> f32 { - // self.glyphs - // .iter() - // .map(|g| g.line_height()) - // .reduce(f32::max) - // // TODO: not 0 - // .unwrap_or(0.0) - // } } /// An iterator of visible text lines, see [`LayoutRun`] @@ -203,10 +194,7 @@ pub struct LayoutRunIter<'b> { line_i: usize, layout_i: usize, remaining_len: usize, - total_layout_height: f32, total_layout: i32, - // TODO: lift this to BufferLine::layout_opt, and take a &'b [f32] slice instead - // line_heights: &'b [f32], } impl<'b> LayoutRunIter<'b> { @@ -226,9 +214,7 @@ impl<'b> LayoutRunIter<'b> { line_i: 0, layout_i: 0, remaining_len: bottom_cropped_layout_lines, - total_layout_height: 0.0, total_layout: 0, - // line_heights, } } } @@ -253,12 +239,10 @@ impl<'b> Iterator for LayoutRunIter<'b> { continue; } // TODO: can scroll be negative? - // let this_line = self.total_layout as usize + self.buffer.scroll as usize - 1; 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(); - // dbg!(line_top); let glyph_height = layout_line.max_ascent + layout_line.max_descent; let centering_offset = (self.buffer.line_heights[this_line] - glyph_height) / 2.0; let line_y = line_top + centering_offset + layout_line.max_ascent; @@ -323,14 +307,6 @@ impl fmt::Display for Metrics { } } -macro_rules! dbgg { - ($ex:expr) => { - if cfg!(debug_assertions) { - println!("{}: {}", stringify!($ex), $ex); - } - }; -} - /// A buffer of text that is shaped and laid out #[derive(Debug)] pub struct Buffer { @@ -417,16 +393,6 @@ impl Buffer { } pub fn line_heights(&self) -> &[f32] { - // self.lines - // .iter() - // .flat_map(|line| line.layout_opt()) - // .flat_map(|lines| lines.iter().map(|line| line.line_height())) - // .collect() - // self.lines - // .iter() - // .flat_map(|line| line.line_heights()) - // .flat_map(|lines| lines.iter().copied()) - // .collect() self.line_heights.as_slice() } @@ -441,7 +407,6 @@ impl Buffer { .flat_map(|line| line.line_heights()) .flat_map(|lines| lines.iter().copied()); self.line_heights.extend(iter); - dbg!(&self.line_heights); #[cfg(all(feature = "std", not(target_arch = "wasm32")))] log::debug!( @@ -459,8 +424,7 @@ impl Buffer { let mut reshaped = 0; let mut total_layout = 0; let mut should_update_line_heights = false; - dbgg!(lines); - for (index, line) in &mut self.lines.iter_mut().enumerate() { + for line in &mut self.lines { if total_layout >= lines { break; } @@ -475,12 +439,6 @@ impl Buffer { let layout = line.layout_in_buffer(&mut self.scratch, font_system, self.width, self.wrap); - // dbg!( - // index, - // total_layout, - // layout.len(), - // total_layout + layout.len() as i32 - // ); total_layout += layout.len() as i32; } @@ -499,7 +457,6 @@ impl Buffer { /// Shape lines until cursor, also scrolling to include cursor in view pub fn shape_until_cursor(&mut self, font_system: &mut FontSystem, cursor: Cursor) { - println!("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); @@ -542,19 +499,7 @@ impl Buffer { // the first visible line is index = self.scroll // the last visible line is index = self.scroll + lines - for (last_line, _) in self - .lines - .iter() - .filter_map(|line| line.layout_opt().as_ref()) - .flat_map(|para| para.iter()) - .enumerate() - .skip(self.scroll as usize) - .take(self.visible_lines() as usize) - { - dbg!(last_line); - } let lines = self.visible_lines(); - dbg!(layout_i, self.scroll, lines); if layout_i < self.scroll { self.scroll = layout_i; } else if layout_i >= self.scroll + lines { @@ -568,20 +513,12 @@ impl Buffer { /// Shape lines until scroll pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem) { - println!( - "{:?} ======================", - std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) - ); - println!("visible_lines() called from shape_until_scroll"); - self.layout_lines(font_system); let lines = self.visible_lines(); let scroll_end = self.scroll + lines; - dbg!(self.scroll, lines, scroll_end); let total_layout = self.shape_until(font_system, scroll_end); - dbg!(total_layout); self.scroll = (total_layout - (lines - 1)).clamp(0, self.scroll); } @@ -754,7 +691,6 @@ impl Buffer { /// 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 { - println!("called visible lines to"); let mut height = self.height; let line_heights = self.line_heights(); if line_heights.is_empty() { @@ -776,22 +712,6 @@ impl Buffer { /// Get the number of lines that can be viewed in the buffer pub fn visible_lines(&self) -> i32 { self.visible_lines_from(self.scroll as usize) - // 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(self.scroll as usize); - // while let Some(line_height) = iter.next() { - // height -= line_height; - // if height <= 0.0 { - // break; - // } - // i += 1; - // } - // i } /// Set text of buffer, using provided attributes for each line by default @@ -927,8 +847,6 @@ impl Buffer { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); - // println!("strike_y: {strike_y}"); - // below, // - `first`, `last` refers to iterator indices (usize) // - `start`, `end` refers to byte indices (usize) @@ -966,14 +884,7 @@ impl Buffer { } } - // println!("{run_index}: [{line_top} .. {line_bot}]"); - if (line_top..line_bot).contains(&strike_y) { - // println!( - // "-> hit on line {run_index} \"{run_text}\"", - // run_text = run.text - // ); - let last_glyph_index = run.glyphs.len() - 1; // TODO: is this assumption correct with rtl? @@ -983,7 +894,6 @@ impl Buffer { (0, last_glyph_index) }; - // TODO: check Arabic for (glyph_index, glyph) in run.glyphs.iter().enumerate() { let glyph_left = glyph.x; @@ -999,34 +909,12 @@ impl Buffer { break 'hit Some(run.cursor_from_glyph_right(glyph)); } - // let glyph_mid = glyph_left + glyph.w / 2.0; - - // println!("{glyph_index}: [{glyph_left} .. {glyph_mid} .. {glyph_right}]"); - - // let hit_glyph = if (glyph_left..glyph_mid).contains(&strike_x) { - // // hit left half of glyph - // println!("-> hit on glyph {glyph_index} (left half)"); - // Some(true) - // } else if (glyph_mid..glyph_right).contains(&strike_x) { - // // hit right half of glyph - // println!("-> hit on glyph {glyph_index} (right half)"); - // Some(false) - // } else { - // None - // }; - - // if let Some(glyph_left_half) = hit_glyph { if (glyph_left..glyph_right).contains(&strike_x) { let cluster = &run.text[glyph.start..glyph.end]; - // println!( - // "--> hit on glyph with value \"{cluster}\" [{len}]", - // len = cluster.len() - // ); let total = cluster.graphemes(true).count(); let last_egc_index = total - 1; let egc_w = glyph.w / (total as f32); - // dbg!(egc_w, total); let mut egc_left = glyph_left; // TODO: is this assumption correct with rtl? @@ -1040,9 +928,6 @@ impl Buffer { cluster.grapheme_indices(true).enumerate() { let egc_end = egc_start + egc.len(); - // if egc_start != 0 { - // dbg!(egc_start); - // } let (left_egc_byte, right_egc_byte) = if glyph.level.is_rtl() { (glyph.start + egc_end, glyph.start + egc_start) @@ -1052,7 +937,6 @@ impl Buffer { if egc_index == left_egc_index && strike_x < egc_left { // hit left of left-most egc in cluster - // println!("-> hit left of left-most egc in cluster"); break 'hit Some(Cursor::new(run.line_i, left_egc_byte)); } @@ -1060,28 +944,22 @@ impl Buffer { if egc_index == right_egc_index && strike_x >= egc_right { // hit right of right-most egc in cluster - // println!("-> hit right of right-most egc in cluster"); break 'hit Some(Cursor::new(run.line_i, right_egc_byte)); } let egc_mid = egc_left + egc_w / 2.0; - // println!("{egc_index}: [{egc_left} .. {egc_mid} .. {egc_right}]"); - let hit_egc = if (egc_left..egc_mid).contains(&strike_x) { // hit left half of egc - // println!("-> hit on egc {egc_index} (left half)"); Some(true) } else if (egc_mid..egc_right).contains(&strike_x) { // hit right half of egc - // println!("-> hit on egc {egc_index} (right half)"); Some(false) } else { None }; if let Some(egc_left_half) = hit_egc { - // println!("--> hit on egc with value \"{egc}\""); break 'hit Some(Cursor::new( run.line_i, if egc_left_half { diff --git a/src/edit/editor.rs b/src/edit/editor.rs index a08601c58d..26cb5ee7f2 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -2,10 +2,7 @@ #[cfg(not(feature = "std"))] use alloc::string::String; -use core::{ - cmp::{self, Ordering}, - iter::once, -}; +use core::{cmp, iter::once}; use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] @@ -480,7 +477,6 @@ impl Edit for Editor { _ => break, } } - dbg!(current_line, self.buffer.scroll()); // let lines = px / self.buffer.metrics().line_height as i32; // match lines.cmp(&0) { // Ordering::Less => { From 3f774c4e91c7e71350a57eadddbcbe51a533c301 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 15:39:57 +0800 Subject: [PATCH 11/36] temp fix Editor Action::Delete not redrawing --- src/edit/editor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 26cb5ee7f2..594eb18535 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -588,6 +588,8 @@ impl Edit for Editor { let old_line = self.buffer.lines.remove(self.cursor.line + 1); self.buffer.lines[self.cursor.line].append(old_line); } + // TODO: Delete doesn't redraw without this now + self.buffer.set_redraw(true); } Action::Click { x, y } => { self.select_opt = None; From b1b054ed5990e026f44dfdec7c8f6b873b5e0fae Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 18:18:32 +0800 Subject: [PATCH 12/36] fix rare case --- src/buffer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 04033c862b..ce8f6034d8 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -852,11 +852,13 @@ impl Buffer { // - `start`, `end` refers to byte indices (usize) // - `left`, `top`, `right`, `bot`, `mid` refers to spatial coordinates (f32) - let last_run_index = self.layout_runs().count() - 1; + let Some(last_run_index) = self.layout_runs().count().checked_sub(1) else { + return None; + }; let mut runs = self.layout_runs().enumerate(); - // TODO: cache line_top, run.line_height(), and line_bot on LayoutRun + // 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 From 205313de00601a83c6e803461815d8d79d51f23d Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 18:27:43 +0800 Subject: [PATCH 13/36] add `enum LineHeight` --- examples/rich-text/src/main.rs | 32 +++++++++++++++++----------- src/attrs.rs | 38 ++++++++++++++++++++++++++++++++++ src/layout.rs | 9 +++----- src/shape.rs | 10 +++++++++ 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index be0cb97fb8..6eae0b16cd 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use cosmic_text::{ - Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, Style, - SwashCache, Weight, + Action, Attrs, AttrsList, Buffer, BufferLine, Color, Edit, Editor, Family, FontSystem, + LineHeight, Metrics, Shaping, Style, SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{ @@ -46,9 +46,10 @@ fn main() { .buffer_mut() .set_size(window.width() as f32, window.height() as f32); - let font_size = editor.buffer().metrics().font_size_; - let attrs = Attrs::new().size(font_size); - let serif_attrs = attrs.family(Family::Serif).size(attrs.font_size * 1.5); + let attrs = Attrs::new() + .size(editor.buffer().metrics().font_size_) + .line_height(LineHeight::Absolute(editor.buffer().metrics().line_height_)); + let serif_attrs = attrs.family(Family::Serif); let mono_attrs = attrs.family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); @@ -102,43 +103,50 @@ fn main() { "Red ", attrs .color(Color::rgb(0xFF, 0x00, 0x00)) - .size(attrs.font_size * 1.9), + .size(attrs.font_size * 1.9) + .line_height(LineHeight::Proportional(0.9)), ), ( "Orange ", attrs .color(Color::rgb(0xFF, 0x7F, 0x00)) - .size(attrs.font_size * 1.6), + .size(attrs.font_size * 1.6) + .line_height(LineHeight::Proportional(1.0)), ), ( "Yellow ", attrs .color(Color::rgb(0xFF, 0xFF, 0x00)) - .size(attrs.font_size * 1.3), + .size(attrs.font_size * 1.3) + .line_height(LineHeight::Proportional(1.1)), ), ( "Green ", attrs .color(Color::rgb(0x00, 0xFF, 0x00)) - .size(attrs.font_size * 1.0), + .size(attrs.font_size * 1.0) + .line_height(LineHeight::Proportional(1.2)), ), ( "Blue ", attrs .color(Color::rgb(0x00, 0x00, 0xFF)) - .size(attrs.font_size * 0.8), + .size(attrs.font_size * 0.8) + .line_height(LineHeight::Proportional(1.3)), ), ( "Indigo ", attrs .color(Color::rgb(0x4B, 0x00, 0x82)) - .size(attrs.font_size * 0.6), + .size(attrs.font_size * 0.6) + .line_height(LineHeight::Proportional(1.4)), ), ( "Violet ", attrs .color(Color::rgb(0x94, 0x00, 0xD3)) - .size(attrs.font_size * 0.4), + .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))), diff --git a/src/attrs.rs b/src/attrs.rs index 8c00ad8f89..da507318f0 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -99,6 +99,31 @@ 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 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, PartialEq)] pub struct Attrs<'a> { @@ -113,6 +138,8 @@ pub struct Attrs<'a> { pub metadata: usize, // TODO: extract pub font_size: f32, + // TODO: extract + pub line_height: LineHeight, } impl<'a> Attrs<'a> { @@ -127,6 +154,7 @@ impl<'a> Attrs<'a> { style: Style::Normal, weight: Weight::NORMAL, font_size: 16.0, + line_height: LineHeight::Proportional(1.2), metadata: 0, } } @@ -143,6 +171,12 @@ impl<'a> Attrs<'a> { self } + /// Set line_height + pub fn line_height(mut self, line_height: LineHeight) -> Self { + self.line_height = line_height; + self + } + /// Set [Family] pub fn family(mut self, family: Family<'a>) -> Self { self.family = family; @@ -219,6 +253,8 @@ pub struct AttrsOwned { pub metadata: usize, // TODO: extract pub font_size: f32, + // TODO: extract + pub line_height: LineHeight, } impl AttrsOwned { @@ -231,6 +267,7 @@ impl AttrsOwned { weight: attrs.weight, metadata: attrs.metadata, font_size: attrs.font_size, + line_height: attrs.line_height, } } @@ -243,6 +280,7 @@ impl AttrsOwned { weight: self.weight, metadata: self.metadata, font_size: self.font_size, + line_height: self.line_height, } } } diff --git a/src/layout.rs b/src/layout.rs index 62ac995c55..c1658d00b0 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 @@ -63,11 +65,6 @@ pub struct PhysicalGlyph { } impl LayoutGlyph { - pub fn line_height(&self) -> f32 { - // TODO: should not be hardcoded / should come from Attrs - self.font_size * 1.2 - } - pub fn physical(&self, offset: (f32, f32), scale: f32) -> PhysicalGlyph { let x_offset = self.font_size * self.x_offset; let y_offset = self.font_size * self.y_offset; @@ -103,7 +100,7 @@ impl LayoutLine { pub fn line_height(&self) -> f32 { self.glyphs .iter() - .map(|g| g.line_height()) + .map(|g| g.line_height) .reduce(f32::max) .unwrap_or_default() } diff --git a/src/shape.rs b/src/shape.rs index 5dc207a67a..3b14f03714 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -147,6 +147,8 @@ fn shape_fallback( 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), }); } @@ -360,9 +362,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), } }), ); @@ -387,6 +394,8 @@ pub struct ShapeGlyph { pub metadata: usize, // TODO: extract pub font_size: f32, + // TODO: extract + pub line_height: f32, } impl ShapeGlyph { @@ -403,6 +412,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, } From d30f947fc719ba4bb4e3fc3d7f31b8361e4e96e7 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 18:29:20 +0800 Subject: [PATCH 14/36] add `Attrs::scale()` --- examples/rich-text/src/main.rs | 5 +++-- src/attrs.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 6eae0b16cd..4bbd970011 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -47,8 +47,9 @@ fn main() { .set_size(window.width() as f32, window.height() as f32); let attrs = Attrs::new() - .size(editor.buffer().metrics().font_size_) - .line_height(LineHeight::Absolute(editor.buffer().metrics().line_height_)); + .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")); diff --git a/src/attrs.rs b/src/attrs.rs index da507318f0..a757181732 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -223,6 +223,14 @@ impl<'a> Attrs<'a> { && self.style == other.style && self.weight == other.weight } + + pub fn scale(mut self, scale: f32) -> Self { + 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> {} From de1284fe6bd0d17da263eb79903406aaef5c39b1 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 23:30:48 +0800 Subject: [PATCH 15/36] add guards for font size and line height --- src/attrs.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/attrs.rs b/src/attrs.rs index a757181732..5a40d428b7 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -166,13 +166,24 @@ impl<'a> Attrs<'a> { } /// Set font size + /// + /// # Panics + /// + /// Will panic if font size is zero. pub fn size(mut self, size: f32) -> Self { + assert_ne!(inner, 0.0, "font size cannot be 0"); self.font_size = size; self } - /// Set line_height + /// Set line height + /// + /// # Panics + /// + /// Will panic if line height is zero. pub fn line_height(mut self, line_height: LineHeight) -> Self { + let LineHeight::Absolute(inner) | LineHeight::Proportional(inner) = line_height; + assert_ne!(inner, 0.0, "line height cannot be 0"); self.line_height = line_height; self } @@ -224,7 +235,13 @@ impl<'a> Attrs<'a> { && 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); From 19d3c6d9d845cd0e3eb45c7f73dcc0d425e2eae3 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 23:31:43 +0800 Subject: [PATCH 16/36] remove `Metrics` --- examples/rich-text/src/main.rs | 6 +-- src/buffer.rs | 78 ++-------------------------------- 2 files changed, 5 insertions(+), 79 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 4bbd970011..7307936ec7 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -2,7 +2,7 @@ use cosmic_text::{ Action, Attrs, AttrsList, Buffer, BufferLine, Color, Edit, Editor, Family, FontSystem, - LineHeight, Metrics, Shaping, Style, SwashCache, Weight, + LineHeight, Shaping, Style, SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{ @@ -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); diff --git a/src/buffer.rs b/src/buffer.rs index ce8f6034d8..5e471d5a7b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -5,7 +5,6 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::fmt; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -275,45 +274,12 @@ 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 fn new(font_size: f32, line_height: f32) -> Self { - Self { - font_size_: font_size, - // TODO: remove this (not hardcoded) - line_height_: font_size * 1.2, - } - } - - 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 pub lines: Vec, line_heights: Vec, - metrics: Metrics, width: f32, height: f32, scroll: i32, @@ -333,16 +299,10 @@ impl Buffer { /// 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(), line_heights: Vec::new(), - metrics, width: 0.0, height: 0.0, scroll: 0, @@ -353,12 +313,8 @@ 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); + pub fn new(font_system: &mut FontSystem) -> Self { + let mut buffer = Self::new_empty(); buffer.set_text(font_system, "", Attrs::new(), Shaping::Advanced); buffer } @@ -605,25 +561,6 @@ impl Buffer { } } - /// Get the current [`Metrics`] - pub fn metrics(&self) -> Metrics { - self.metrics - } - - /// 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) { - if metrics != self.metrics { - assert_ne!(metrics.font_size_, 0.0, "font size cannot be 0"); - self.metrics = metrics; - self.relayout(font_system); - self.shape_until_scroll(font_system); - } - } - /// Get the current [`Wrap`] pub fn wrap(&self) -> Wrap { self.wrap @@ -1052,15 +989,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); From d8a2892cd9e3d8fdf35749e0fd1431b639930c78 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Fri, 8 Sep 2023 23:34:46 +0800 Subject: [PATCH 17/36] fix compile errors --- src/attrs.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/attrs.rs b/src/attrs.rs index 5a40d428b7..932be4de88 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -166,23 +166,25 @@ impl<'a> Attrs<'a> { } /// Set font size - /// + /// /// # Panics - /// + /// /// Will panic if font size is zero. pub fn size(mut self, size: f32) -> Self { - assert_ne!(inner, 0.0, "font size cannot be 0"); + 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 LineHeight::Absolute(inner) | LineHeight::Proportional(inner) = line_height; + 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 @@ -236,9 +238,9 @@ impl<'a> Attrs<'a> { } /// 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"); From 6c275ad237e12448f224d03b23c0a698cecf990b Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 2 Oct 2023 22:15:41 +0800 Subject: [PATCH 18/36] remove unused imports in example --- examples/rich-text/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 7307936ec7..da7efa7412 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use cosmic_text::{ - Action, Attrs, AttrsList, Buffer, BufferLine, Color, Edit, Editor, Family, FontSystem, - LineHeight, Shaping, Style, SwashCache, Weight, + Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, LineHeight, Shaping, Style, + SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{ From 64c2b76b4db0d2fd657641f2a077dbe74522bfda Mon Sep 17 00:00:00 2001 From: tigregalis Date: Mon, 2 Oct 2023 22:43:43 +0800 Subject: [PATCH 19/36] add `LineHeight` to `Hash` impl for `Attrs` --- src/attrs.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/attrs.rs b/src/attrs.rs index 932be4de88..ff9632fdb7 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -109,6 +109,21 @@ pub enum LineHeight { 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 { @@ -323,6 +338,7 @@ impl core::hash::Hash for AttrsOwned { self.weight.hash(state); self.metadata.hash(state); self.font_size.to_bits().hash(state); + self.line_height.hash(state); } } From 0ae11e0e6f5cb6aefb3de7dbb19f03849642e74b Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 3 Oct 2023 21:13:06 +0800 Subject: [PATCH 20/36] update BufferLine layout_in_buffer to match layout --- src/buffer_line.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/buffer_line.rs b/src/buffer_line.rs index e5dd9b09e5..4e2e00ebdd 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -214,8 +214,11 @@ impl BufferLine { self.wrap = wrap; let align = self.align; let shape = self.shape(font_system); + let layout = shape.layout(width, wrap, align); + let line_heights = layout.iter().map(|line| line.line_height()).collect(); + self.layout_opt = Some(layout); self.line_heights = Some(line_heights); } @@ -223,6 +226,8 @@ impl BufferLine { } /// 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, @@ -234,9 +239,14 @@ impl BufferLine { 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, width, wrap, align, &mut layout); + + let line_heights = layout.iter().map(|line| line.line_height()).collect(); + self.layout_opt = Some(layout); + self.line_heights = Some(line_heights); } self.layout_opt.as_ref().expect("layout not found") } From 65334fa104c0b1e6b3e5703ffe7829cb99179835 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Tue, 3 Oct 2023 21:19:27 +0800 Subject: [PATCH 21/36] impl BufferLine layout using layout_in_buffer --- src/buffer_line.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 4e2e00ebdd..d97ee51cbc 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -210,19 +210,7 @@ impl BufferLine { 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(width, wrap, align); - - let line_heights = layout.iter().map(|line| line.line_height()).collect(); - - self.layout_opt = Some(layout); - self.line_heights = Some(line_heights); - } - 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. From d8def162fff6b79c3a24f39c5cb9330d8ab6cbaa Mon Sep 17 00:00:00 2001 From: tigregalis Date: Wed, 4 Oct 2023 22:23:55 +0800 Subject: [PATCH 22/36] combine BufferLine layout_opt and line_heights --- src/buffer.rs | 4 ++-- src/buffer_line.rs | 35 +++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 5e471d5a7b..f2849027eb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -228,7 +228,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; @@ -482,7 +482,7 @@ impl Buffer { 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 = diff --git a/src/buffer_line.rs b/src/buffer_line.rs index d97ee51cbc..f9ee38bd43 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -12,8 +12,7 @@ pub struct BufferLine { wrap: Wrap, align: Option, shape_opt: Option, - layout_opt: Option>, - line_heights: Option>, + layout_opt: Option, shaping: Shaping, } @@ -29,7 +28,6 @@ impl BufferLine { align: None, shape_opt: None, layout_opt: None, - line_heights: None, shaping, } } @@ -157,13 +155,11 @@ impl BufferLine { pub fn reset(&mut self) { self.shape_opt = None; self.layout_opt = None; - self.line_heights = None; } /// Reset only layout information pub fn reset_layout(&mut self) { self.layout_opt = None; - self.line_heights = None; } /// Check if shaping and layout information is cleared @@ -191,7 +187,6 @@ impl BufferLine { self.shaping, )); self.layout_opt = None; - self.line_heights = None; } self.shape_opt.as_ref().expect("shape not found") } @@ -233,19 +228,31 @@ impl BufferLine { let line_heights = layout.iter().map(|line| line.line_height()).collect(); - self.layout_opt = Some(layout); - self.line_heights = Some(line_heights); + 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, maps from [`Self::layout_opt`] - pub fn line_heights(&self) -> &Option> { - &self.line_heights + /// Get line height cache + pub fn line_heights(&self) -> Option<&[f32]> { + self.layout_opt.as_ref().map(|l| l.line_heights.as_ref()) } } + +/// A list of [`LayoutLine`] in a [`BufferLine`] alongside their line heights +#[derive(Debug)] +struct LayoutLines { + layout: Vec, + line_heights: Vec, +} From a12018341e07d6628c05635f5f00cc5a9149583c Mon Sep 17 00:00:00 2001 From: tigregalis Date: Wed, 4 Oct 2023 22:35:15 +0800 Subject: [PATCH 23/36] add missing line_height hash --- src/attrs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/attrs.rs b/src/attrs.rs index ff9632fdb7..e6ad15640f 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -278,6 +278,7 @@ impl<'a> core::hash::Hash for Attrs<'a> { self.weight.hash(state); self.metadata.hash(state); self.font_size.to_bits().hash(state); + self.line_height.hash(state); } } From ef09ab3db7f419da63cbf27265fe2825a441d110 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 7 Oct 2023 07:16:34 +0800 Subject: [PATCH 24/36] add more docs to Buffer --- src/buffer.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index f2849027eb..48106bf693 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -279,14 +279,18 @@ impl<'b> ExactSizeIterator for LayoutRunIter<'b> {} pub struct Buffer { /// [BufferLine]s (or paragraphs) of text in the buffer pub lines: Vec, + /// The cached heights of visual lines 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, } @@ -348,10 +352,12 @@ impl Buffer { 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(); From 2e82f625d3d63511b17d60597e9240200af07602 Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 7 Oct 2023 07:51:41 +0800 Subject: [PATCH 25/36] add more docs to buffer --- src/buffer.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 48106bf693..a25636f4e1 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,5 +1,16 @@ // 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, ToString}, @@ -112,9 +123,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, @@ -277,17 +288,17 @@ impl<'b> ExactSizeIterator for LayoutRunIter<'b> {} /// 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, - /// The cached heights of visual lines + /// The cached heights of visual lines. Compute using [`Buffer::update_line_heights`]. line_heights: Vec, - /// The text bounding box width + /// The text bounding box width. width: f32, - /// The text bounding box height + /// The text bounding box height. height: f32, - /// The current scroll position in terms of visual lines + /// The current scroll position in terms of visual lines. scroll: i32, - /// True if a redraw is required. Set to false after processing + /// True if a redraw is required. Set to false after processing. redraw: bool, /// The wrapping mode wrap: Wrap, @@ -652,7 +663,7 @@ impl Buffer { i } - /// Get the number of lines that can be viewed in the buffer + /// Get the number of visual lines that can be viewed in the buffer pub fn visible_lines(&self) -> i32 { self.visible_lines_from(self.scroll as usize) } From 506c3844d3bac2a3f1472ff63d1bd415040358ea Mon Sep 17 00:00:00 2001 From: tigregalis Date: Sat, 7 Oct 2023 07:52:33 +0800 Subject: [PATCH 26/36] clamp Buffer::set_scroll --- src/buffer.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index a25636f4e1..df8ba72086 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -610,13 +610,17 @@ impl Buffer { } } - /// 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; From fb81f106f3c5d7b649faf46fcebb286ef26f0ca5 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 15 Nov 2023 21:50:55 +0000 Subject: [PATCH 27/36] Fix terminal example --- examples/terminal/src/main.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/terminal/src/main.rs b/examples/terminal/src/main.rs index d5fbeda7c7..1d40ff805e 100644 --- a/examples/terminal/src/main.rs +++ b/examples/terminal/src/main.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use cosmic_text::{Attrs, Buffer, Color, FontSystem, Metrics, Shaping, SwashCache}; +use cosmic_text::{Attrs, Buffer, Color, FontSystem, LineHeight, Shaping, SwashCache}; use std::cmp::{self, Ordering}; use termion::{color, cursor}; @@ -12,10 +12,10 @@ fn main() { 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); + // 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); let mut buffer = buffer.borrow_with(&mut font_system); @@ -25,7 +25,9 @@ fn main() { buffer.set_size(width as f32, height as f32); // Attributes indicate what font to choose - let attrs = Attrs::new(); + let attrs = Attrs::new() + .size(14.0) + .line_height(LineHeight::Absolute(20.0)); // Add some text! buffer.set_text(" Hi, Rust! 🦀", attrs, Shaping::Advanced); From ca44fdae0c7aafb24f8dae645d68ee5582fcba9e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 15 Nov 2023 22:17:13 +0000 Subject: [PATCH 28/36] Fix editor-libcosmic example --- examples/editor-libcosmic/src/main.rs | 52 +++++++++++++++-------- examples/editor-libcosmic/src/text_box.rs | 22 ++++++---- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index 84f27d2316..72bf0f7632 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() + .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 + .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..d347fe16f6 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -138,15 +138,21 @@ where .buffer_mut() .shape_until(i32::max_value()); - let mut layout_lines = 0; + let mut height = 0.0; for line in editor.buffer().lines.iter() { + let line_height = line + .attrs_list() + .spans() + .iter() + .map(|(_, attrs)| attrs.line_height.height(attrs.font_size)) + .max_by(f32::total_cmp) + .unwrap_or(0.0); match line.layout_opt() { - Some(layout) => layout_lines += layout.len(), + Some(layout) => height += layout.len() as f32 * line_height, None => (), } } - let height = layout_lines as f32 * editor.buffer().metrics().line_height; let size = Size::new(limits.max().width, height); log::info!("size {:?}", size); @@ -219,10 +225,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 +252,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( From d5d8bd3893baeb44b58ecabbe087e73b2bc52db5 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 15 Nov 2023 22:23:34 +0000 Subject: [PATCH 29/36] Use line_heights method instead of manual computation --- examples/editor-libcosmic/src/text_box.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index d347fe16f6..00c59b2a89 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -138,21 +138,7 @@ where .buffer_mut() .shape_until(i32::max_value()); - let mut height = 0.0; - for line in editor.buffer().lines.iter() { - let line_height = line - .attrs_list() - .spans() - .iter() - .map(|(_, attrs)| attrs.line_height.height(attrs.font_size)) - .max_by(f32::total_cmp) - .unwrap_or(0.0); - match line.layout_opt() { - Some(layout) => height += layout.len() as f32 * line_height, - None => (), - } - } - + let height = editor.buffer().line_heights().iter().sum(); let size = Size::new(limits.max().width, height); log::info!("size {:?}", size); From c48ccbff08e389abfad92f8d68caa62ca6f45877 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 15 Nov 2023 22:30:14 +0000 Subject: [PATCH 30/36] Fix test compilation --- tests/common/mod.rs | 15 ++++++++++----- tests/wrap_stability.rs | 7 ++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9e250f84fb..a54a047ffe 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() + .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..013888d1aa 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) + .size(font_size), ); let mut font_system = FontSystem::new_with_locale_and_db("en-US".into(), fontdb::Database::new()); @@ -23,11 +24,11 @@ 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 layout_unbounded = line.layout(start_width, wrap, Some(Align::Left)); let max_width = layout_unbounded.iter().map(|l| l.w).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 layout_bounded = line.layout(new_limit, wrap, Some(Align::Left)); let bounded_max_width = layout_bounded.iter().map(|l| l.w).fold(0.0, f32::max); // For debugging: From 4a7dddf15a012ca4b7ff5a090ab60aabe7561187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sat, 9 Dec 2023 17:21:06 +0100 Subject: [PATCH 31/36] Empty lines get a span attached, and those spans are used to determine the height of the line by providing an empty_height value to the layout function, that sets the line height for lines without characters --- examples/rich-text/src/main.rs | 6 +++++ rich-text.sh | 2 +- src/buffer.rs | 29 ++++++++++++++++------- src/buffer_line.rs | 14 ++++++++--- src/edit/editor.rs | 30 +++++++++++++----------- src/layout.rs | 39 ++++++++++++++++++++++++++---- src/lib.rs | 7 ++---- src/shape.rs | 43 +++++++++++++++++----------------- tests/wrap_stability.rs | 10 ++++---- 9 files changed, 118 insertions(+), 62 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 0c4e66edcc..d971334643 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -154,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/buffer.rs b/src/buffer.rs index 43787cbb9b..afd46695ac 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -265,7 +265,7 @@ 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], } }); @@ -302,7 +302,7 @@ pub struct Buffer { } 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, @@ -322,7 +322,7 @@ impl Buffer { } } - /// Create a new [`Buffer`] with the provided [`FontSystem`] and [`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); @@ -678,9 +678,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, @@ -763,8 +763,19 @@ 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)); + //TODO: there can be newlines, that has their own span attributes, "\n" lines for example + let attr_list = match &maybe_span { + Some((attr, range)) => { + let mut list = AttrsList::new(default_attrs); + list.add_span(range.clone(), attr.to_owned()); + list + } + None => { + println!("defaultattrs"); + 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); @@ -1025,9 +1036,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 f9ee38bd43..c3e948bff6 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -221,10 +221,17 @@ impl BufferLine { 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, width, wrap, align, &mut 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(); @@ -246,7 +253,8 @@ impl BufferLine { /// Get line height cache pub fn line_heights(&self) -> Option<&[f32]> { - self.layout_opt.as_ref().map(|l| l.line_heights.as_ref()) + let r = self.layout_opt.as_ref().map(|l| l.line_heights.as_ref()); + r } } diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 81bfa9c157..f46cdbdf19 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -163,20 +163,22 @@ impl 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( diff --git a/src/layout.rs b/src/layout.rs index c1658d00b0..48ecabe501 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -87,22 +87,53 @@ impl LayoutGlyph { #[derive(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 { - pub fn line_height(&self) -> f32 { - self.glyphs + /// 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_default() + .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) } } 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 6f22d95610..91da5951cb 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -652,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, @@ -866,7 +866,13 @@ impl ShapeLine { runs } - pub fn layout(&self, line_width: f32, wrap: Wrap, align: Option) -> Vec { + pub fn layout( + &self, + 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(), @@ -874,6 +880,7 @@ impl ShapeLine { wrap, align, &mut lines, + empty_height, ); lines } @@ -885,6 +892,8 @@ impl ShapeLine { 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 @@ -1259,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_descent, - 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/wrap_stability.rs b/tests/wrap_stability.rs index 013888d1aa..44ac7a69c5 100644 --- a/tests/wrap_stability.rs +++ b/tests/wrap_stability.rs @@ -24,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(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(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); @@ -41,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}\"",); } }; From 1a92c3b103da1e610db527ca1e9af4e393d55454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sun, 10 Dec 2023 15:02:51 +0100 Subject: [PATCH 32/36] always add spans, even if they are the same as default --- src/buffer.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index afd46695ac..bb439bf678 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -746,10 +746,7 @@ impl Buffer { let text_start = line_string.len(); line_string.push_str(text); let text_end = line_string.len(); - // Only add attrs if they don't match the defaults - if *attrs != attrs_list.defaults() { - attrs_list.add_span(text_start..text_end, *attrs); - } + attrs_list.add_span(text_start..text_end, *attrs); } // we know that at the end of a line, From 6d3e44978b8f0e7ba5bee5e51081bb63f81d0efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sun, 10 Dec 2023 15:08:36 +0100 Subject: [PATCH 33/36] nit: font_size instead of size --- examples/editor-libcosmic/src/main.rs | 4 ++-- examples/rich-text/src/main.rs | 16 ++++++++-------- examples/terminal/src/main.rs | 2 +- src/attrs.rs | 2 +- tests/common/mod.rs | 2 +- tests/wrap_stability.rs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index 72bf0f7632..ef6c616492 100644 --- a/examples/editor-libcosmic/src/main.rs +++ b/examples/editor-libcosmic/src/main.rs @@ -70,7 +70,7 @@ impl FontSize { pub fn to_attrs(self) -> Attrs<'static> { Attrs::new() - .size(self.to_font_size()) + .font_size(self.to_font_size()) .line_height(self.to_line_height()) } } @@ -252,7 +252,7 @@ impl Application for Window { self.font_size = font_size; self.attrs = self .attrs - .size(font_size.to_font_size()) + .font_size(font_size.to_font_size()) .line_height(font_size.to_line_height()); let mut editor = self.editor.lock().unwrap(); diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index d971334643..801cdd9d6c 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -45,7 +45,7 @@ fn main() { .set_size(window.width() as f32, window.height() as f32); let attrs = Attrs::new() - .size(32.0) + .font_size(32.0) .line_height(LineHeight::Absolute(44.0)) .scale(display_scale); let serif_attrs = attrs.family(Family::Serif); @@ -102,49 +102,49 @@ fn main() { "Red ", attrs .color(Color::rgb(0xFF, 0x00, 0x00)) - .size(attrs.font_size * 1.9) + .font_size(attrs.font_size * 1.9) .line_height(LineHeight::Proportional(0.9)), ), ( "Orange ", attrs .color(Color::rgb(0xFF, 0x7F, 0x00)) - .size(attrs.font_size * 1.6) + .font_size(attrs.font_size * 1.6) .line_height(LineHeight::Proportional(1.0)), ), ( "Yellow ", attrs .color(Color::rgb(0xFF, 0xFF, 0x00)) - .size(attrs.font_size * 1.3) + .font_size(attrs.font_size * 1.3) .line_height(LineHeight::Proportional(1.1)), ), ( "Green ", attrs .color(Color::rgb(0x00, 0xFF, 0x00)) - .size(attrs.font_size * 1.0) + .font_size(attrs.font_size * 1.0) .line_height(LineHeight::Proportional(1.2)), ), ( "Blue ", attrs .color(Color::rgb(0x00, 0x00, 0xFF)) - .size(attrs.font_size * 0.8) + .font_size(attrs.font_size * 0.8) .line_height(LineHeight::Proportional(1.3)), ), ( "Indigo ", attrs .color(Color::rgb(0x4B, 0x00, 0x82)) - .size(attrs.font_size * 0.6) + .font_size(attrs.font_size * 0.6) .line_height(LineHeight::Proportional(1.4)), ), ( "Violet ", attrs .color(Color::rgb(0x94, 0x00, 0xD3)) - .size(attrs.font_size * 0.4) + .font_size(attrs.font_size * 0.4) .line_height(LineHeight::Proportional(1.5)), ), ("U", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), diff --git a/examples/terminal/src/main.rs b/examples/terminal/src/main.rs index 1d40ff805e..b5f84eb648 100644 --- a/examples/terminal/src/main.rs +++ b/examples/terminal/src/main.rs @@ -26,7 +26,7 @@ fn main() { // Attributes indicate what font to choose let attrs = Attrs::new() - .size(14.0) + .font_size(14.0) .line_height(LineHeight::Absolute(20.0)); // Add some text! diff --git a/src/attrs.rs b/src/attrs.rs index e6ad15640f..f73fd1ec49 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -185,7 +185,7 @@ impl<'a> Attrs<'a> { /// # Panics /// /// Will panic if font size is zero. - pub fn size(mut self, size: f32) -> Self { + pub fn font_size(mut self, size: f32) -> Self { assert_ne!(size, 0.0, "font size cannot be 0"); self.font_size = size; self diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a54a047ffe..c4f86cd98b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -94,7 +94,7 @@ impl DrawTestCfg { &self.text, self.font .as_attrs() - .size(self.font_size) + .font_size(self.font_size) .line_height(cosmic_text::LineHeight::Absolute(self.line_height)), Shaping::Advanced, ); diff --git a/tests/wrap_stability.rs b/tests/wrap_stability.rs index 44ac7a69c5..64f8954659 100644 --- a/tests/wrap_stability.rs +++ b/tests/wrap_stability.rs @@ -14,7 +14,7 @@ fn stable_wrap() { Attrs::new() .family(Family::Name("FiraMono")) .weight(Weight::MEDIUM) - .size(font_size), + .font_size(font_size), ); let mut font_system = FontSystem::new_with_locale_and_db("en-US".into(), fontdb::Database::new()); From bd31f68b56d32121462890dff319decf75b6322d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Sun, 10 Dec 2023 16:13:14 +0100 Subject: [PATCH 34/36] Revert "always add spans, even if they are the same as default" This reverts commit 1a92c3b103da1e610db527ca1e9af4e393d55454. --- src/buffer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/buffer.rs b/src/buffer.rs index bb439bf678..afd46695ac 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -746,7 +746,10 @@ impl Buffer { let text_start = line_string.len(); line_string.push_str(text); let text_end = line_string.len(); - attrs_list.add_span(text_start..text_end, *attrs); + // Only add attrs if they don't match the defaults + if *attrs != attrs_list.defaults() { + attrs_list.add_span(text_start..text_end, *attrs); + } } // we know that at the end of a line, From 25380f82e8b1a5e4199cf9f075dc31221fab1b50 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 11 Dec 2023 13:01:48 -0700 Subject: [PATCH 35/36] Fix no_std build --- src/buffer.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 1b62b13324..ef227c3629 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -13,6 +13,7 @@ #[cfg(not(feature = "std"))] use alloc::{ + borrow::ToOwned, string::{String, ToString}, vec::Vec, }; @@ -770,10 +771,7 @@ impl Buffer { list.add_span(range.clone(), attr.to_owned()); list } - None => { - println!("defaultattrs"); - AttrsList::new(default_attrs) - } + 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); From 16ff8d305a970bd9baad2bb5b364a017372ef23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20=C3=96dmark?= Date: Mon, 11 Dec 2023 21:52:21 +0100 Subject: [PATCH 36/36] only add newline spans, if they dont match the defaults --- src/buffer.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index ef227c3629..ba20b5c538 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -764,11 +764,15 @@ impl Buffer { maybe_line = lines_iter.next(); if maybe_line.is_some() { // finalize this line and start a new line - //TODO: there can be newlines, that has their own span attributes, "\n" lines for example + 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); - list.add_span(range.clone(), attr.to_owned()); + if *attr != attrs_list.defaults() { + list.add_span(range.clone(), attr.to_owned()); + } list } None => AttrsList::new(default_attrs),