diff --git a/benches/layout.rs b/benches/layout.rs index d41dc46..5b174ea 100644 --- a/benches/layout.rs +++ b/benches/layout.rs @@ -10,7 +10,7 @@ fn load_font_system(c: &mut Criterion) { fn layout(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(10.0, 10.0)); - buffer.set_size(&mut fs, Some(80.0), None); + buffer.set_size(Some(80.0), None); for (wrap_name, wrap) in &[ ("None", ct::Wrap::None), @@ -22,11 +22,11 @@ fn layout(c: &mut Criterion) { ("Advanced", ct::Shaping::Advanced), ] { let mut group = c.benchmark_group(format!("Wrap({wrap_name}, {shape_name})")); - buffer.set_wrap(&mut fs, *wrap); + buffer.set_wrap(*wrap); let mut run_on_text = |text: &str| { buffer.lines.clear(); - buffer.set_text(&mut fs, text, &ct::Attrs::new(), *shape, None); + buffer.set_text(text, &ct::Attrs::new(), *shape, None); buffer.shape_until_scroll(&mut fs, false); }; diff --git a/benches/text_shaping_benchmarks.rs b/benches/text_shaping_benchmarks.rs index 097b552..cfba990 100644 --- a/benches/text_shaping_benchmarks.rs +++ b/benches/text_shaping_benchmarks.rs @@ -5,14 +5,13 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn bench_ascii_fast_path(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); - buffer.set_size(&mut fs, Some(500.0), None); + buffer.set_size(Some(500.0), None); let ascii_text = "Pure ASCII text for BidiParagraphs optimization testing.\n".repeat(50); c.bench_function("ShapeLine/ASCII Fast Path", |b| { b.iter(|| { buffer.set_text( - &mut fs, black_box(&ascii_text), &ct::Attrs::new(), ct::Shaping::Advanced, @@ -26,14 +25,13 @@ fn bench_ascii_fast_path(c: &mut Criterion) { fn bench_bidi_processing(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); - buffer.set_size(&mut fs, Some(500.0), None); + buffer.set_size(Some(500.0), None); let bidi_text = "Mixed English and العربية النص العربي text for BiDi testing.\nThis tests adjust_levels and combined BiDi optimizations.\n".repeat(30); c.bench_function("ShapeLine/BiDi Processing", |b| { b.iter(|| { buffer.set_text( - &mut fs, black_box(&bidi_text), &ct::Attrs::new(), ct::Shaping::Advanced, @@ -47,7 +45,7 @@ fn bench_bidi_processing(c: &mut Criterion) { fn bench_lang_mixed(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); - buffer.set_size(&mut fs, Some(500.0), None); + buffer.set_size(Some(500.0), None); let bidi_text = include_str!("../sample/hello.txt"); @@ -56,7 +54,6 @@ fn bench_lang_mixed(c: &mut Criterion) { .bench_function("ShapeLine/Mixed-Language Text", |b| { b.iter(|| { buffer.set_text( - &mut fs, black_box(&bidi_text), &ct::Attrs::new(), ct::Shaping::Advanced, @@ -70,14 +67,13 @@ fn bench_lang_mixed(c: &mut Criterion) { fn bench_layout_heavy(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); - buffer.set_size(&mut fs, Some(500.0), None); + buffer.set_size(Some(500.0), None); let layout_text = "This is a very long line that will wrap multiple times and stress the reorder optimization through intensive layout processing with comprehensive buffer reuse testing. ".repeat(30); c.bench_function("ShapeLine/Layout Heavy", |b| { b.iter(|| { buffer.set_text( - &mut fs, black_box(&layout_text), &ct::Attrs::new(), ct::Shaping::Advanced, @@ -91,7 +87,7 @@ fn bench_layout_heavy(c: &mut Criterion) { fn bench_combined_stress(c: &mut Criterion) { let mut fs = ct::FontSystem::new(); let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); - buffer.set_size(&mut fs, Some(500.0), None); + buffer.set_size(Some(500.0), None); let stress_text = format!("{}\n{}\n{}\n{}\n", "ASCII line for BidiParagraphs optimization. ".repeat(15), @@ -103,7 +99,6 @@ fn bench_combined_stress(c: &mut Criterion) { c.bench_function("ShapeLine/Combined Stress", |b| { b.iter(|| { buffer.set_text( - &mut fs, black_box(&stress_text), &ct::Attrs::new(), ct::Shaping::Advanced, diff --git a/src/buffer.rs b/src/buffer.rs index 2579c71..d5f3540 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -16,6 +16,21 @@ use crate::{ Wrap, }; +bitflags::bitflags! { + /// Tracks which buffer-wide properties have changed since the last layout. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + struct DirtyFlags: u8 { + /// Layout caches are stale (wrap, size, metrics, hinting, ellipsize, monospace_width changed) + const RELAYOUT = 0b0001; + /// tab_width changed — lines containing tabs need reshape + const TAB_SHAPE = 0b0010; + /// Text was replaced via set_text/set_rich_text — lines are fresh, just need shape_until_scroll + const TEXT_SET = 0b0100; + /// Scroll position changed — visible region may have shifted to unshaped lines + const SCROLL = 0b1000; + } +} + /// A line of visible text for rendering #[derive(Debug)] pub struct LayoutRun<'a> { @@ -330,6 +345,8 @@ pub struct Buffer { monospace_width: Option, tab_width: u16, hinting: Hinting, + /// Dirty flags tracking which properties changed since last layout + dirty: DirtyFlags, } impl Clone for Buffer { @@ -346,6 +363,7 @@ impl Clone for Buffer { monospace_width: self.monospace_width, tab_width: self.tab_width, hinting: self.hinting, + dirty: self.dirty, } } } @@ -376,6 +394,7 @@ impl Buffer { monospace_width: None, tab_width: 8, hinting: Hinting::default(), + dirty: DirtyFlags::empty(), } } @@ -386,7 +405,8 @@ impl Buffer { /// 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); - buffer.set_text(font_system, "", &Attrs::new(), Shaping::Advanced, None); + buffer.set_text("", &Attrs::new(), Shaping::Advanced, None); + buffer.shape_until_scroll(font_system, false); buffer } @@ -401,30 +421,36 @@ impl Buffer { } } - fn relayout(&mut self, font_system: &mut FontSystem) { - #[cfg(all(feature = "std", not(target_arch = "wasm32")))] - let instant = std::time::Instant::now(); + /// Process dirty flags: invalidate shape/layout caches as needed, then clear flags. + /// Returns `true` if any flags were set (i.e., work may be needed). + fn resolve_dirty(&mut self) -> bool { + let dirty = self.dirty; + if dirty.is_empty() { + return false; + } - 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_opt, - self.wrap, - self.ellipsize, - self.monospace_width, - self.tab_width, - self.hinting, - ); + if dirty.contains(DirtyFlags::TEXT_SET) { + // Lines were replaced — already fresh, no cache to invalidate. + } else { + if dirty.contains(DirtyFlags::TAB_SHAPE) { + for line in &mut self.lines { + if line.shape_opt().is_some() && line.text().contains('\t') { + line.reset_shaping(); + } + } + } + if dirty.contains(DirtyFlags::RELAYOUT) { + for line in &mut self.lines { + if line.shape_opt().is_some() { + line.reset_layout(); + } + } } } self.redraw = true; - - #[cfg(all(feature = "std", not(target_arch = "wasm32")))] - log::debug!("relayout: {:?}", instant.elapsed()); + self.dirty = DirtyFlags::empty(); + true } /// Shape lines until cursor, also scrolling to include cursor in view @@ -435,6 +461,7 @@ impl Buffer { cursor: Cursor, prune: bool, ) { + self.shape_until_scroll(font_system, prune); let metrics = self.metrics; let old_scroll = self.scroll; @@ -490,7 +517,7 @@ impl Buffer { } if old_scroll != self.scroll { - self.redraw = true; + self.dirty |= DirtyFlags::SCROLL; } self.shape_until_scroll(font_system, prune); @@ -524,9 +551,22 @@ impl Buffer { } } - /// Shape lines until scroll + /// Shape lines until scroll, resolving any pending dirty state first. + /// + /// This processes dirty flags (invalidating caches for lines that need + /// reshaping or relayout) and then shapes/layouts visible lines. + /// + /// Call this before reading layout results via [`layout_runs`] or [`hit`] + /// when working with the `Buffer` directly. The [`BorrowedWithFontSystem`] + /// wrapper calls this automatically. + /// + /// [`layout_runs`]: Self::layout_runs + /// [`hit`]: Self::hit #[allow(clippy::missing_panics_doc)] pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { + if !self.resolve_dirty() { + return; + } let metrics = self.metrics; let old_scroll = self.scroll; @@ -670,13 +710,19 @@ impl Buffer { self.metrics } - /// Set the current [`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) { - self.set_metrics_and_size(font_system, metrics, self.width_opt, self.height_opt); + pub fn set_metrics(&mut self, metrics: Metrics) { + if metrics != self.metrics { + assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); + assert_ne!(metrics.line_height, 0.0, "line height cannot be 0"); + self.metrics = metrics; + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; + } } /// Get the current [`Hinting`] strategy. @@ -685,11 +731,11 @@ impl Buffer { } /// Set the current [`Hinting`] strategy. - pub fn set_hinting(&mut self, font_system: &mut FontSystem, hinting: Hinting) { + pub fn set_hinting(&mut self, hinting: Hinting) { if hinting != self.hinting { self.hinting = hinting; - self.relayout(font_system); - self.shape_until_scroll(font_system, false); + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; } } @@ -698,12 +744,12 @@ impl Buffer { self.wrap } - /// Set the current [`Wrap`] - pub fn set_wrap(&mut self, font_system: &mut FontSystem, wrap: Wrap) { + /// Set the current [`Wrap`]. + pub fn set_wrap(&mut self, wrap: Wrap) { if wrap != self.wrap { self.wrap = wrap; - self.relayout(font_system); - self.shape_until_scroll(font_system, false); + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; } } @@ -712,12 +758,12 @@ impl Buffer { self.ellipsize } - /// Set the current [`Ellipsize`] - pub fn set_ellipsize(&mut self, font_system: &mut FontSystem, ellipsize: Ellipsize) { + /// Set the current [`Ellipsize`]. + pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) { if ellipsize != self.ellipsize { self.ellipsize = ellipsize; - self.relayout(font_system); - self.shape_until_scroll(font_system, false); + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; } } @@ -726,16 +772,12 @@ impl Buffer { self.monospace_width } - /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize - pub fn set_monospace_width( - &mut self, - font_system: &mut FontSystem, - monospace_width: Option, - ) { + /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize. + pub fn set_monospace_width(&mut self, monospace_width: Option) { if monospace_width != self.monospace_width { self.monospace_width = monospace_width; - self.relayout(font_system); - self.shape_until_scroll(font_system, false); + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; } } @@ -744,22 +786,15 @@ impl Buffer { self.tab_width } - /// Set tab width (number of spaces between tab stops) - pub fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { - // A tab width of 0 is not allowed + /// Set tab width (number of spaces between tab stops). + pub fn set_tab_width(&mut self, tab_width: u16) { if tab_width == 0 { return; } if tab_width != self.tab_width { self.tab_width = tab_width; - // Shaping must be reset when tab width is changed - for line in &mut self.lines { - if line.shape_opt().is_some() && line.text().contains('\t') { - line.reset_shaping(); - } - } + self.dirty |= DirtyFlags::TAB_SHAPE | DirtyFlags::RELAYOUT; self.redraw = true; - self.shape_until_scroll(font_system, false); } } @@ -768,42 +803,35 @@ impl Buffer { (self.width_opt, self.height_opt) } - /// Set the current buffer dimensions - pub fn set_size( - &mut self, - font_system: &mut FontSystem, - width_opt: Option, - height_opt: Option, - ) { - self.set_metrics_and_size(font_system, self.metrics, width_opt, height_opt); + /// Set the current buffer dimensions. + pub fn set_size(&mut self, width_opt: Option, height_opt: Option) { + let width_clamped = width_opt.map(|v| v.max(0.0)); + let height_clamped = height_opt.map(|v| v.max(0.0)); + if width_clamped != self.width_opt { + self.width_opt = width_clamped; + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; + } + if height_clamped != self.height_opt { + self.height_opt = height_clamped; + self.dirty |= DirtyFlags::RELAYOUT; + self.redraw = true; + } } - /// Set the current [`Metrics`] and buffer dimensions at the same time + /// Set the current [`Metrics`] and buffer dimensions at the same time. /// /// # Panics /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics_and_size( &mut self, - font_system: &mut FontSystem, metrics: Metrics, width_opt: Option, height_opt: Option, ) { - let clamped_width_opt = width_opt.map(|width| width.max(0.0)); - let clamped_height_opt = height_opt.map(|height| height.max(0.0)); - - if metrics != self.metrics - || clamped_width_opt != self.width_opt - || clamped_height_opt != self.height_opt - { - assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); - self.metrics = metrics; - self.width_opt = clamped_width_opt; - self.height_opt = clamped_height_opt; - self.relayout(font_system); - self.shape_until_scroll(font_system, false); - } + self.set_metrics(metrics); + self.set_size(width_opt, height_opt); } /// Get the current scroll location @@ -815,45 +843,73 @@ impl Buffer { pub fn set_scroll(&mut self, scroll: Scroll) { if scroll != self.scroll { self.scroll = scroll; + self.dirty |= DirtyFlags::SCROLL; self.redraw = true; } } - /// Set text of buffer, using provided attributes for each line by default - pub fn set_text( + /// Internal: set text of buffer, reusing existing line allocations. + /// + /// Does NOT call `shape_until_scroll` — the caller is responsible for that. + fn set_text_impl( &mut self, - font_system: &mut FontSystem, text: &str, attrs: &Attrs, shaping: Shaping, alignment: Option, ) { - self.lines.clear(); + let mut line_count = 0; for (range, ending) in LineIter::new(text) { - self.lines.push(BufferLine::new( - &text[range], - ending, - AttrsList::new(attrs), - shaping, - )); + let line_text = &text[range]; + if line_count < self.lines.len() { + // Reuse existing line: reclaim String/AttrsList allocations + let mut reused_text = self.lines[line_count].reclaim_text(); + reused_text.push_str(line_text); + let reused_attrs = self.lines[line_count].reclaim_attrs().reset(attrs); + self.lines[line_count].reset_new(reused_text, ending, reused_attrs, shaping); + } else { + self.lines.push(BufferLine::new( + line_text, + ending, + AttrsList::new(attrs), + shaping, + )); + } + line_count += 1; } - // Ensure there is an ending line with no line ending - if self - .lines - .last() - .map(|line| line.ending()) - .unwrap_or_default() - != LineEnding::None - { - self.lines.push(BufferLine::new( - "", - LineEnding::None, - AttrsList::new(attrs), - shaping, - )); + // Ensure there is an ending line with no line ending. + // When no lines were produced (empty text), unwrap_or_default() returns + // LineEnding::Lf (the Default), which is != None, so we add an empty line. + let last_ending = if line_count > 0 { + self.lines[line_count - 1].ending() + } else { + LineEnding::default() + }; + if last_ending != LineEnding::None { + if line_count < self.lines.len() { + let reused_text = self.lines[line_count].reclaim_text(); + let reused_attrs = self.lines[line_count].reclaim_attrs().reset(attrs); + self.lines[line_count].reset_new( + reused_text, + LineEnding::None, + reused_attrs, + shaping, + ); + } else { + self.lines.push(BufferLine::new( + "", + LineEnding::None, + AttrsList::new(attrs), + shaping, + )); + } + line_count += 1; } + // Discard excess lines now that we have reused as much of the existing allocations as possible. + self.lines.truncate(line_count); + if alignment.is_some() { self.lines.iter_mut().for_each(|line| { line.set_align(alignment); @@ -861,30 +917,26 @@ impl Buffer { } self.scroll = Scroll::default(); - self.shape_until_scroll(font_system, false); } - /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) - /// - /// ``` - /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; - /// # let mut font_system = FontSystem::new(); - /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); - /// let attrs = Attrs::new().family(Family::Serif); - /// buffer.set_rich_text( - /// &mut font_system, - /// [ - /// ("hello, ", attrs.clone()), - /// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), - /// ], - /// &attrs, - /// Shaping::Advanced, - /// None, - /// ); - /// ``` - pub fn set_rich_text<'r, 's, I>( + /// Set text of buffer, using provided attributes for each line by default. + pub fn set_text( + &mut self, + text: &str, + attrs: &Attrs, + shaping: Shaping, + alignment: Option, + ) { + self.set_text_impl(text, attrs, shaping, alignment); + self.dirty |= DirtyFlags::TEXT_SET; + self.redraw = true; + } + + /// Internal: set rich text of buffer, reusing existing line allocations. + /// + /// Does NOT call `shape_until_scroll` — the caller is responsible for that. + fn set_rich_text_impl<'r, 's, I>( &mut self, - font_system: &mut FontSystem, spans: I, default_attrs: &Attrs, shaping: Shaping, @@ -1017,8 +1069,37 @@ impl Buffer { }); self.scroll = Scroll::default(); + } - self.shape_until_scroll(font_system, false); + /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes). + /// + /// ``` + /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; + /// # let mut font_system = FontSystem::new(); + /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); + /// let attrs = Attrs::new().family(Family::Serif); + /// buffer.set_rich_text( + /// [ + /// ("hello, ", attrs.clone()), + /// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), + /// ], + /// &attrs, + /// Shaping::Advanced, + /// None, + /// ); + /// ``` + pub fn set_rich_text<'r, 's, I>( + &mut self, + spans: I, + default_attrs: &Attrs, + shaping: Shaping, + alignment: Option, + ) where + I: IntoIterator)>, + { + self.set_rich_text_impl(spans, default_attrs, shaping, alignment); + self.dirty |= DirtyFlags::TEXT_SET; + self.redraw = true; } /// True if a redraw is needed @@ -1031,12 +1112,24 @@ impl Buffer { self.redraw = redraw; } - /// Get the visible layout runs for rendering and other tasks + /// Get the visible layout runs for rendering and other tasks. + /// + /// This returns an iterator over the laid-out runs that are visible in the + /// current scroll region. Call [`shape_until_scroll`] first to ensure the buffer + /// is up to date, or use [`BorrowedWithFontSystem`] which calls it + /// automatically. + /// + /// [`shape_until_scroll`]: Self::shape_until_scroll pub fn layout_runs(&self) -> LayoutRunIter<'_> { LayoutRunIter::new(self) } - /// Convert x, y position to Cursor (hit detection) + /// Convert x, y position to Cursor (hit detection). + /// + /// Call [`shape_until_scroll`] first to ensure the buffer is up to date, + /// or use [`BorrowedWithFontSystem`] which calls it automatically. + /// + /// [`shape_until_scroll`]: Self::shape_until_scroll pub fn hit(&self, x: f32, y: f32) -> Option { #[cfg(all(feature = "std", not(target_arch = "wasm32")))] let instant = std::time::Instant::now(); @@ -1478,10 +1571,12 @@ impl Buffer { Some((cursor, cursor_x_opt)) } - /// Draw the buffer + /// Draw the buffer. + /// + /// Automatically resolves any pending dirty state before drawing. #[cfg(feature = "swash")] pub fn draw( - &self, + &mut self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, color: Color, @@ -1489,15 +1584,32 @@ impl Buffer { ) where F: FnMut(i32, i32, u32, u32, Color), { + self.shape_until_scroll(font_system, false); let mut renderer = crate::LegacyRenderer { font_system, cache, callback, }; - self.render(&mut renderer, color); + for run in self.layout_runs() { + for glyph in run.glyphs { + let physical_glyph = glyph.physical((0., run.line_y), 1.0); + let glyph_color = glyph.color_opt.map_or(color, |some| some); + renderer.glyph(physical_glyph, glyph_color); + } + render_decoration(&mut renderer, &run, color); + } } - pub fn render(&self, renderer: &mut R, color: Color) { + /// Render the buffer using the provided renderer. + /// + /// Automatically resolves any pending dirty state before rendering. + pub fn render( + &mut self, + font_system: &mut FontSystem, + renderer: &mut R, + color: Color, + ) { + self.shape_until_scroll(font_system, false); for run in self.layout_runs() { for glyph in run.glyphs { let physical_glyph = glyph.physical((0., run.line_y), 1.0); @@ -1517,11 +1629,6 @@ impl BorrowedWithFontSystem<'_, Buffer> { .shape_until_cursor(self.font_system, cursor, prune); } - /// Shape lines until scroll - pub fn shape_until_scroll(&mut self, prune: bool) { - self.inner.shape_until_scroll(self.font_system, prune); - } - /// Shape the provided line index and return the result pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> { self.inner.line_shape(self.font_system, line_i) @@ -1532,31 +1639,36 @@ impl BorrowedWithFontSystem<'_, Buffer> { self.inner.line_layout(self.font_system, line_i) } - /// Set the current [`Metrics`] + /// 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); + self.inner.set_metrics(metrics); } - /// Set the current [`Wrap`] + /// Set the current [`Hinting`] strategy. + pub fn set_hinting(&mut self, hinting: Hinting) { + self.inner.set_hinting(hinting); + } + + /// Set the current [`Wrap`]. pub fn set_wrap(&mut self, wrap: Wrap) { - self.inner.set_wrap(self.font_system, wrap); + self.inner.set_wrap(wrap); } - /// Set the current [`Ellipsize`] + /// Set the current [`Ellipsize`]. pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) { - self.inner.set_ellipsize(self.font_system, ellipsize); + self.inner.set_ellipsize(ellipsize); } - /// Set the current buffer dimensions + /// Set the current buffer dimensions. pub fn set_size(&mut self, width_opt: Option, height_opt: Option) { - self.inner.set_size(self.font_system, width_opt, height_opt); + self.inner.set_size(width_opt, height_opt); } - /// Set the current [`Metrics`] and buffer dimensions at the same time + /// Set the current [`Metrics`] and buffer dimensions at the same time. /// /// # Panics /// @@ -1568,15 +1680,22 @@ impl BorrowedWithFontSystem<'_, Buffer> { height_opt: Option, ) { self.inner - .set_metrics_and_size(self.font_system, metrics, width_opt, height_opt); + .set_metrics_and_size(metrics, width_opt, height_opt); } - /// Set tab width (number of spaces between tab stops) + /// Set tab width (number of spaces between tab stops). + /// + /// A `tab_width` of 0 is ignored. pub fn set_tab_width(&mut self, tab_width: u16) { - self.inner.set_tab_width(self.font_system, tab_width); + self.inner.set_tab_width(tab_width); } - /// Set text of buffer, using provided attributes for each line by default + /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize. + pub fn set_monospace_width(&mut self, monospace_width: Option) { + self.inner.set_monospace_width(monospace_width); + } + + /// Set text of buffer, using provided attributes for each line by default. pub fn set_text( &mut self, text: &str, @@ -1584,28 +1703,10 @@ impl BorrowedWithFontSystem<'_, Buffer> { shaping: Shaping, alignment: Option, ) { - self.inner - .set_text(self.font_system, text, attrs, shaping, alignment); + self.inner.set_text(text, attrs, shaping, alignment); } - /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) - /// - /// ``` - /// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; - /// # let mut font_system = FontSystem::new(); - /// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); - /// let attrs = Attrs::new().family(Family::Serif); - /// buffer.set_rich_text( - /// &mut font_system, - /// [ - /// ("hello, ", attrs.clone()), - /// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), - /// ], - /// &attrs, - /// Shaping::Advanced, - /// None, - /// ); - /// ``` + /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes). pub fn set_rich_text<'r, 's, I>( &mut self, spans: I, @@ -1616,7 +1717,30 @@ impl BorrowedWithFontSystem<'_, Buffer> { I: IntoIterator)>, { self.inner - .set_rich_text(self.font_system, spans, default_attrs, shaping, alignment); + .set_rich_text(spans, default_attrs, shaping, alignment); + } + + /// Shape lines until scroll, resolving any pending dirty state first. + /// + /// See [`Buffer::shape_until_scroll`]. + pub fn shape_until_scroll(&mut self, prune: bool) { + self.inner.shape_until_scroll(self.font_system, prune); + } + + /// Get the visible layout runs for rendering and other tasks. + /// + /// Automatically resolves any pending dirty state. + pub fn layout_runs(&mut self) -> LayoutRunIter<'_> { + self.inner.shape_until_scroll(self.font_system, false); + self.inner.layout_runs() + } + + /// Convert x, y position to Cursor (hit detection). + /// + /// Automatically resolves any pending dirty state. + pub fn hit(&mut self, x: f32, y: f32) -> Option { + self.inner.shape_until_scroll(self.font_system, false); + self.inner.hit(x, y) } /// Apply a [`Motion`] to a [`Cursor`] @@ -1630,7 +1754,9 @@ impl BorrowedWithFontSystem<'_, Buffer> { .cursor_motion(self.font_system, cursor, cursor_x_opt, motion) } - /// Draw the buffer + /// Draw the buffer. + /// + /// Automatically resolves any pending dirty state. #[cfg(feature = "swash")] pub fn draw(&mut self, cache: &mut crate::SwashCache, color: Color, f: F) where diff --git a/src/edit/editor.rs b/src/edit/editor.rs index bea7bbc..51bfdc3 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -47,10 +47,12 @@ impl<'buffer> Editor<'buffer> { } /// Draw the editor + /// + /// Automatically resolves any pending dirty state before drawing. #[cfg(feature = "swash")] #[allow(clippy::too_many_arguments)] pub fn draw( - &self, + &mut self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, text_color: Color, @@ -61,6 +63,7 @@ impl<'buffer> Editor<'buffer> { ) where F: FnMut(i32, i32, u32, u32, Color), { + self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, false)); let mut renderer = crate::LegacyRenderer { font_system, cache, @@ -75,6 +78,10 @@ impl<'buffer> Editor<'buffer> { ); } + /// Render the editor using the provided renderer. + /// + /// The caller is responsible for calling [`Edit::shape_as_needed`] first + /// to ensure layout is up to date. pub fn render( &self, renderer: &mut R, @@ -207,8 +214,8 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { self.with_buffer(super::super::buffer::Buffer::tab_width) } - fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { - self.with_buffer_mut(|buffer| buffer.set_tab_width(font_system, tab_width)); + fn set_tab_width(&mut self, tab_width: u16) { + self.with_buffer_mut(|buffer| buffer.set_tab_width(tab_width)); } fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { @@ -764,8 +771,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { Action::Click { x, y } => { self.set_selection(Selection::None); - if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) - { + if let Some(new_cursor) = self.with_buffer_mut(|buffer| { + buffer.shape_until_scroll(font_system, false); + buffer.hit(x as f32, y as f32) + }) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); @@ -775,8 +784,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { Action::DoubleClick { x, y } => { self.set_selection(Selection::None); - if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) - { + if let Some(new_cursor) = self.with_buffer_mut(|buffer| { + buffer.shape_until_scroll(font_system, false); + buffer.hit(x as f32, y as f32) + }) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); @@ -788,8 +799,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { Action::TripleClick { x, y } => { self.set_selection(Selection::None); - if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) - { + if let Some(new_cursor) = self.with_buffer_mut(|buffer| { + buffer.shape_until_scroll(font_system, false); + buffer.hit(x as f32, y as f32) + }) { if new_cursor != self.cursor { self.cursor = new_cursor; } @@ -803,8 +816,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> { self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } - if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) - { + if let Some(new_cursor) = self.with_buffer_mut(|buffer| { + buffer.shape_until_scroll(font_system, false); + buffer.hit(x as f32, y as f32) + }) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); diff --git a/src/edit/mod.rs b/src/edit/mod.rs index daad378..42f8115 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -292,7 +292,7 @@ pub trait Edit<'buffer> { fn tab_width(&self) -> u16; /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored - fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16); + fn set_tab_width(&mut self, tab_width: u16); /// Shape lines until scroll, after adjusting scroll if the cursor moved fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool); @@ -351,7 +351,7 @@ impl<'buffer, E: Edit<'buffer>> BorrowedWithFontSystem<'_, E> { /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored pub fn set_tab_width(&mut self, tab_width: u16) { - self.inner.set_tab_width(self.font_system, tab_width); + self.inner.set_tab_width(tab_width); } /// Shape lines until scroll, after adjusting scroll if the cursor moved diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index ce22d3d..392825d 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -107,7 +107,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> { #[cfg(feature = "std")] pub fn load_text>( &mut self, - font_system: &mut FontSystem, + _font_system: &mut FontSystem, path: P, mut attrs: crate::Attrs, ) -> io::Result<()> { @@ -125,7 +125,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> { // Clear buffer first (allows sane handling of non-existant files) self.editor.with_buffer_mut(|buffer| { - buffer.set_text(font_system, "", &attrs, Shaping::Advanced, None); + buffer.set_text("", &attrs, Shaping::Advanced, None); }); // Update syntax based on file name @@ -147,7 +147,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> { // Set text let text = fs::read_to_string(path)?; self.editor.with_buffer_mut(|buffer| { - buffer.set_text(font_system, &text, &attrs, Shaping::Advanced, None); + buffer.set_text(&text, &attrs, Shaping::Advanced, None); }); Ok(()) @@ -218,19 +218,32 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> { } /// Draw the editor + /// + /// Automatically resolves any pending dirty state before drawing. #[cfg(feature = "swash")] - pub fn draw(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F) - where + pub fn draw( + &mut self, + font_system: &mut FontSystem, + cache: &mut crate::SwashCache, + callback: F, + ) where F: FnMut(i32, i32, u32, u32, Color), { - let mut renderer = crate::LegacyRenderer { + self.editor.draw( font_system, cache, + self.foreground_color(), + self.cursor_color(), + self.selection_color(), + self.foreground_color(), callback, - }; - self.render(&mut renderer); + ); } + /// Render the editor using the provided renderer. + /// + /// The caller is responsible for calling [`Edit::shape_as_needed`] first + /// to ensure layout is up to date. pub fn render(&self, renderer: &mut R) { let size = self.with_buffer(|buffer| buffer.size()); if let Some(width) = size.0 { @@ -285,8 +298,8 @@ impl<'buffer> Edit<'buffer> for SyntaxEditor<'_, 'buffer> { self.editor.tab_width() } - fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { - self.editor.set_tab_width(font_system, tab_width); + fn set_tab_width(&mut self, tab_width: u16) { + self.editor.set_tab_width(tab_width); } fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { diff --git a/src/edit/vi.rs b/src/edit/vi.rs index aca3468..b12097b 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -302,11 +302,19 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { self.changed = eval_changed(&self.commands, self.save_pivot); } + /// Draw the editor. + /// + /// Automatically resolves any pending dirty state before drawing. #[cfg(feature = "swash")] - pub fn draw(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F) - where + pub fn draw( + &mut self, + font_system: &mut FontSystem, + cache: &mut crate::SwashCache, + callback: F, + ) where F: FnMut(i32, i32, u32, u32, Color), { + self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, false)); let mut renderer = crate::LegacyRenderer { font_system, cache, @@ -315,6 +323,10 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { self.render(&mut renderer); } + /// Render the editor using the provided renderer. + /// + /// The caller is responsible for calling [`Edit::shape_as_needed`] first + /// to ensure layout is up to date. pub fn render(&self, renderer: &mut R) { let background_color = self.background_color(); let foreground_color = self.foreground_color(); @@ -553,8 +565,8 @@ impl<'buffer> Edit<'buffer> for ViEditor<'_, 'buffer> { self.editor.tab_width() } - fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { - self.editor.set_tab_width(font_system, tab_width); + fn set_tab_width(&mut self, tab_width: u16) { + self.editor.set_tab_width(tab_width); } fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { diff --git a/src/lib.rs b/src/lib.rs index 0cad691..fbf797e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,18 +29,13 @@ //! // Borrow buffer together with the font system for more convenient method calls //! let mut buffer = buffer.borrow_with(&mut font_system); //! -//! // Set a size for the text buffer, in pixels -//! buffer.set_size(Some(80.0), Some(25.0)); -//! //! // Attributes indicate what font to choose //! let attrs = Attrs::new(); //! -//! // Add some text! +//! // Set size and text +//! buffer.set_size(Some(80.0), Some(25.0)); //! buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced, None); //! -//! // Perform shaping as desired -//! buffer.shape_until_scroll(true); -//! //! // Inspect the output runs //! for run in buffer.layout_runs() { //! for glyph in run.glyphs.iter() { diff --git a/src/shape.rs b/src/shape.rs index 82e30a9..5e187af 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1262,6 +1262,11 @@ impl ShapeLine { shaping: Shaping, tab_width: u16, ) { + // Clear stale ellipsis span so it gets recomputed with the current attrs. + // Without this, reusing a ShapeLine from a previous text (via Cached::Unused) + // would keep an ellipsis shaped with the old attrs. + self.ellipsis_span = None; + let mut spans = mem::take(&mut self.spans); // Cache the shape spans in reverse order so they can be popped for reuse in the same order. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a9ae318..99751b6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -149,7 +149,6 @@ impl DrawTestCfg { self.alignment, ); } - buffer.shape_until_scroll(true); // Black let text_color = Color::rgb(0x00, 0x00, 0x00); diff --git a/tests/richtext_layout.rs b/tests/richtext_layout.rs index e6280d3..f43cefd 100644 --- a/tests/richtext_layout.rs +++ b/tests/richtext_layout.rs @@ -28,7 +28,6 @@ fn empty_lines_use_span_metrics() { None, ); buffer.set_size(Some(500.0), Some(500.0)); - buffer.shape_until_scroll(false); let line_heights: Vec = buffer.layout_runs().map(|run| run.line_height).collect(); diff --git a/tests/shaping_and_rendering.rs b/tests/shaping_and_rendering.rs index 6ef28da..f0ce6fe 100644 --- a/tests/shaping_and_rendering.rs +++ b/tests/shaping_and_rendering.rs @@ -88,7 +88,7 @@ fn test_ligature_segmentation() { let mut buffer = buffer.borrow_with(&mut font_system); buffer.set_text("|>", &Attrs::new(), Shaping::Advanced, None); - buffer.shape_until_scroll(false); + let _ = buffer.layout_runs(); let line = &buffer.lines[0]; let shape = line.shape_opt().expect("ShapeLine not found"); @@ -105,7 +105,7 @@ fn test_ligature_segmentation() { // Test -> (Arrow), which is a common ligature. buffer.set_text("->", &Attrs::new(), Shaping::Advanced, None); - buffer.shape_until_scroll(false); + let _ = buffer.layout_runs(); let line = &buffer.lines[0]; let shape = line.shape_opt().expect("ShapeLine not found"); @@ -118,7 +118,7 @@ fn test_ligature_segmentation() { // Test != buffer.set_text("!=", &Attrs::new(), Shaping::Advanced, None); - buffer.shape_until_scroll(false); + let _ = buffer.layout_runs(); let line = &buffer.lines[0]; let shape = line.shape_opt().expect("ShapeLine not found"); // Inter has a contextual alternate for != too. @@ -131,7 +131,7 @@ fn test_ligature_segmentation() { // Test ++ buffer.set_text("++", &Attrs::new(), Shaping::Advanced, None); - buffer.shape_until_scroll(false); + let _ = buffer.layout_runs(); let line = &buffer.lines[0]; let shape = line.shape_opt().expect("ShapeLine not found"); // Inter does not have a ++ ligature. diff --git a/tests/wrap_stability.rs b/tests/wrap_stability.rs index 56a51a2..ba27d56 100644 --- a/tests/wrap_stability.rs +++ b/tests/wrap_stability.rs @@ -117,15 +117,10 @@ fn wrap_extra_line() { let mut buffer = buffer.borrow_with(&mut font_system); - // Add some text! + // Configure wrap and size, then add text buffer.set_wrap(Wrap::Word); - buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing\n\nweeewoooo minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced, None); - - // Set a size for the text buffer, in pixels buffer.set_size(Some(50.0), Some(1000.0)); - - // Perform shaping as desired - buffer.shape_until_scroll(false); + buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing\n\nweeewoooo minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced, None); let empty_lines = buffer.layout_runs().filter(|x| x.line_w == 0.).count(); let overflow_lines = buffer.layout_runs().filter(|x| x.line_w > 50.).count(); diff --git a/tests/wrap_word_fallback.rs b/tests/wrap_word_fallback.rs index db7f80a..d1a2443 100644 --- a/tests/wrap_word_fallback.rs +++ b/tests/wrap_word_fallback.rs @@ -15,12 +15,12 @@ fn wrap_word_fallback() { let mut buffer = buffer.borrow_with(&mut font_system); buffer.set_wrap(Wrap::WordOrGlyph); - buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced, None); buffer.set_size(Some(50.0), Some(1000.0)); + buffer.set_text("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.", &Attrs::new().family(cosmic_text::Family::Name("Inter")), Shaping::Advanced, None); - buffer.shape_until_scroll(false); - - let measured_size = measure(&buffer); + let measured_size = buffer + .layout_runs() + .fold(0.0f32, |width, run| width.max(run.line_w)); assert!( measured_size <= buffer.size().0.unwrap_or(0.0), @@ -29,9 +29,3 @@ fn wrap_word_fallback() { buffer.size().0.unwrap_or(0.0) ); } - -fn measure(buffer: &Buffer) -> f32 { - buffer - .layout_runs() - .fold(0.0f32, |width, run| width.max(run.line_w)) -}