feat: buffer setter methods are now lazy

This commit is contained in:
Hojjat 2026-02-25 23:14:56 -07:00 committed by Jeremy Soller
parent e5926aec74
commit 626f44dad8
14 changed files with 388 additions and 240 deletions

View file

@ -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);
};

View file

@ -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,

View file

@ -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<f32>,
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<f32>,
) {
/// 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<f32>) {
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<f32>,
height_opt: Option<f32>,
) {
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<f32>, height_opt: Option<f32>) {
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<f32>,
height_opt: Option<f32>,
) {
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<Align>,
) {
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<Align>,
) {
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<Align>,
) where
I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
{
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<Cursor> {
#[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<F>(
&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<R: Renderer>(&self, renderer: &mut R, color: Color) {
/// Render the buffer using the provided renderer.
///
/// Automatically resolves any pending dirty state before rendering.
pub fn render<R: Renderer>(
&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<f32>, height_opt: Option<f32>) {
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<f32>,
) {
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<f32>) {
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<Align>,
) {
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<Item = (&'s str, Attrs<'r>)>,
{
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<Cursor> {
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<F>(&mut self, cache: &mut crate::SwashCache, color: Color, f: F)
where

View file

@ -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<F>(
&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<R: Renderer>(
&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));

View file

@ -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

View file

@ -107,7 +107,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
#[cfg(feature = "std")]
pub fn load_text<P: AsRef<Path>>(
&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<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F)
where
pub fn draw<F>(
&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<R: Renderer>(&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) {

View file

@ -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<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F)
where
pub fn draw<F>(
&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<R: Renderer>(&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) {

View file

@ -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() {

View file

@ -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.

View file

@ -149,7 +149,6 @@ impl DrawTestCfg {
self.alignment,
);
}
buffer.shape_until_scroll(true);
// Black
let text_color = Color::rgb(0x00, 0x00, 0x00);

View file

@ -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<f32> = buffer.layout_runs().map(|run| run.line_height).collect();

View file

@ -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.

View file

@ -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();

View file

@ -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))
}