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) { fn layout(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(10.0, 10.0)); 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 &[ for (wrap_name, wrap) in &[
("None", ct::Wrap::None), ("None", ct::Wrap::None),
@ -22,11 +22,11 @@ fn layout(c: &mut Criterion) {
("Advanced", ct::Shaping::Advanced), ("Advanced", ct::Shaping::Advanced),
] { ] {
let mut group = c.benchmark_group(format!("Wrap({wrap_name}, {shape_name})")); 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| { let mut run_on_text = |text: &str| {
buffer.lines.clear(); 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); 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) { fn bench_ascii_fast_path(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); 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); let ascii_text = "Pure ASCII text for BidiParagraphs optimization testing.\n".repeat(50);
c.bench_function("ShapeLine/ASCII Fast Path", |b| { c.bench_function("ShapeLine/ASCII Fast Path", |b| {
b.iter(|| { b.iter(|| {
buffer.set_text( buffer.set_text(
&mut fs,
black_box(&ascii_text), black_box(&ascii_text),
&ct::Attrs::new(), &ct::Attrs::new(),
ct::Shaping::Advanced, ct::Shaping::Advanced,
@ -26,14 +25,13 @@ fn bench_ascii_fast_path(c: &mut Criterion) {
fn bench_bidi_processing(c: &mut Criterion) { fn bench_bidi_processing(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); 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); 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| { c.bench_function("ShapeLine/BiDi Processing", |b| {
b.iter(|| { b.iter(|| {
buffer.set_text( buffer.set_text(
&mut fs,
black_box(&bidi_text), black_box(&bidi_text),
&ct::Attrs::new(), &ct::Attrs::new(),
ct::Shaping::Advanced, ct::Shaping::Advanced,
@ -47,7 +45,7 @@ fn bench_bidi_processing(c: &mut Criterion) {
fn bench_lang_mixed(c: &mut Criterion) { fn bench_lang_mixed(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); 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"); 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| { .bench_function("ShapeLine/Mixed-Language Text", |b| {
b.iter(|| { b.iter(|| {
buffer.set_text( buffer.set_text(
&mut fs,
black_box(&bidi_text), black_box(&bidi_text),
&ct::Attrs::new(), &ct::Attrs::new(),
ct::Shaping::Advanced, ct::Shaping::Advanced,
@ -70,14 +67,13 @@ fn bench_lang_mixed(c: &mut Criterion) {
fn bench_layout_heavy(c: &mut Criterion) { fn bench_layout_heavy(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); 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); 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| { c.bench_function("ShapeLine/Layout Heavy", |b| {
b.iter(|| { b.iter(|| {
buffer.set_text( buffer.set_text(
&mut fs,
black_box(&layout_text), black_box(&layout_text),
&ct::Attrs::new(), &ct::Attrs::new(),
ct::Shaping::Advanced, ct::Shaping::Advanced,
@ -91,7 +87,7 @@ fn bench_layout_heavy(c: &mut Criterion) {
fn bench_combined_stress(c: &mut Criterion) { fn bench_combined_stress(c: &mut Criterion) {
let mut fs = ct::FontSystem::new(); let mut fs = ct::FontSystem::new();
let mut buffer = ct::Buffer::new(&mut fs, ct::Metrics::new(14.0, 20.0)); 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", let stress_text = format!("{}\n{}\n{}\n{}\n",
"ASCII line for BidiParagraphs optimization. ".repeat(15), "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| { c.bench_function("ShapeLine/Combined Stress", |b| {
b.iter(|| { b.iter(|| {
buffer.set_text( buffer.set_text(
&mut fs,
black_box(&stress_text), black_box(&stress_text),
&ct::Attrs::new(), &ct::Attrs::new(),
ct::Shaping::Advanced, ct::Shaping::Advanced,

View file

@ -16,6 +16,21 @@ use crate::{
Wrap, 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 /// A line of visible text for rendering
#[derive(Debug)] #[derive(Debug)]
pub struct LayoutRun<'a> { pub struct LayoutRun<'a> {
@ -330,6 +345,8 @@ pub struct Buffer {
monospace_width: Option<f32>, monospace_width: Option<f32>,
tab_width: u16, tab_width: u16,
hinting: Hinting, hinting: Hinting,
/// Dirty flags tracking which properties changed since last layout
dirty: DirtyFlags,
} }
impl Clone for Buffer { impl Clone for Buffer {
@ -346,6 +363,7 @@ impl Clone for Buffer {
monospace_width: self.monospace_width, monospace_width: self.monospace_width,
tab_width: self.tab_width, tab_width: self.tab_width,
hinting: self.hinting, hinting: self.hinting,
dirty: self.dirty,
} }
} }
} }
@ -376,6 +394,7 @@ impl Buffer {
monospace_width: None, monospace_width: None,
tab_width: 8, tab_width: 8,
hinting: Hinting::default(), hinting: Hinting::default(),
dirty: DirtyFlags::empty(),
} }
} }
@ -386,7 +405,8 @@ impl Buffer {
/// Will panic if `metrics.line_height` is zero. /// Will panic if `metrics.line_height` is zero.
pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self { pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
let mut buffer = Self::new_empty(metrics); 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 buffer
} }
@ -401,30 +421,36 @@ impl Buffer {
} }
} }
fn relayout(&mut self, font_system: &mut FontSystem) { /// Process dirty flags: invalidate shape/layout caches as needed, then clear flags.
#[cfg(all(feature = "std", not(target_arch = "wasm32")))] /// Returns `true` if any flags were set (i.e., work may be needed).
let instant = std::time::Instant::now(); fn resolve_dirty(&mut self) -> bool {
let dirty = self.dirty;
if dirty.is_empty() {
return false;
}
for line in &mut self.lines { if dirty.contains(DirtyFlags::TEXT_SET) {
if line.shape_opt().is_some() { // Lines were replaced — already fresh, no cache to invalidate.
line.reset_layout(); } else {
line.layout( if dirty.contains(DirtyFlags::TAB_SHAPE) {
font_system, for line in &mut self.lines {
self.metrics.font_size, if line.shape_opt().is_some() && line.text().contains('\t') {
self.width_opt, line.reset_shaping();
self.wrap, }
self.ellipsize, }
self.monospace_width, }
self.tab_width, if dirty.contains(DirtyFlags::RELAYOUT) {
self.hinting, for line in &mut self.lines {
); if line.shape_opt().is_some() {
line.reset_layout();
}
}
} }
} }
self.redraw = true; self.redraw = true;
self.dirty = DirtyFlags::empty();
#[cfg(all(feature = "std", not(target_arch = "wasm32")))] true
log::debug!("relayout: {:?}", instant.elapsed());
} }
/// Shape lines until cursor, also scrolling to include cursor in view /// Shape lines until cursor, also scrolling to include cursor in view
@ -435,6 +461,7 @@ impl Buffer {
cursor: Cursor, cursor: Cursor,
prune: bool, prune: bool,
) { ) {
self.shape_until_scroll(font_system, prune);
let metrics = self.metrics; let metrics = self.metrics;
let old_scroll = self.scroll; let old_scroll = self.scroll;
@ -490,7 +517,7 @@ impl Buffer {
} }
if old_scroll != self.scroll { if old_scroll != self.scroll {
self.redraw = true; self.dirty |= DirtyFlags::SCROLL;
} }
self.shape_until_scroll(font_system, prune); 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)] #[allow(clippy::missing_panics_doc)]
pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) { pub fn shape_until_scroll(&mut self, font_system: &mut FontSystem, prune: bool) {
if !self.resolve_dirty() {
return;
}
let metrics = self.metrics; let metrics = self.metrics;
let old_scroll = self.scroll; let old_scroll = self.scroll;
@ -670,13 +710,19 @@ impl Buffer {
self.metrics self.metrics
} }
/// Set the current [`Metrics`] /// Set the current [`Metrics`].
/// ///
/// # Panics /// # Panics
/// ///
/// Will panic if `metrics.font_size` is zero. /// Will panic if `metrics.font_size` is zero.
pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { pub fn set_metrics(&mut self, metrics: Metrics) {
self.set_metrics_and_size(font_system, metrics, self.width_opt, self.height_opt); 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. /// Get the current [`Hinting`] strategy.
@ -685,11 +731,11 @@ impl Buffer {
} }
/// Set the current [`Hinting`] strategy. /// 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 { if hinting != self.hinting {
self.hinting = hinting; self.hinting = hinting;
self.relayout(font_system); self.dirty |= DirtyFlags::RELAYOUT;
self.shape_until_scroll(font_system, false); self.redraw = true;
} }
} }
@ -698,12 +744,12 @@ impl Buffer {
self.wrap self.wrap
} }
/// Set the current [`Wrap`] /// Set the current [`Wrap`].
pub fn set_wrap(&mut self, font_system: &mut FontSystem, wrap: Wrap) { pub fn set_wrap(&mut self, wrap: Wrap) {
if wrap != self.wrap { if wrap != self.wrap {
self.wrap = wrap; self.wrap = wrap;
self.relayout(font_system); self.dirty |= DirtyFlags::RELAYOUT;
self.shape_until_scroll(font_system, false); self.redraw = true;
} }
} }
@ -712,12 +758,12 @@ impl Buffer {
self.ellipsize self.ellipsize
} }
/// Set the current [`Ellipsize`] /// Set the current [`Ellipsize`].
pub fn set_ellipsize(&mut self, font_system: &mut FontSystem, ellipsize: Ellipsize) { pub fn set_ellipsize(&mut self, ellipsize: Ellipsize) {
if ellipsize != self.ellipsize { if ellipsize != self.ellipsize {
self.ellipsize = ellipsize; self.ellipsize = ellipsize;
self.relayout(font_system); self.dirty |= DirtyFlags::RELAYOUT;
self.shape_until_scroll(font_system, false); self.redraw = true;
} }
} }
@ -726,16 +772,12 @@ impl Buffer {
self.monospace_width self.monospace_width
} }
/// Set monospace width monospace glyphs should be resized to match. `None` means don't resize /// Set monospace width monospace glyphs should be resized to match. `None` means don't resize.
pub fn set_monospace_width( pub fn set_monospace_width(&mut self, monospace_width: Option<f32>) {
&mut self,
font_system: &mut FontSystem,
monospace_width: Option<f32>,
) {
if monospace_width != self.monospace_width { if monospace_width != self.monospace_width {
self.monospace_width = monospace_width; self.monospace_width = monospace_width;
self.relayout(font_system); self.dirty |= DirtyFlags::RELAYOUT;
self.shape_until_scroll(font_system, false); self.redraw = true;
} }
} }
@ -744,22 +786,15 @@ impl Buffer {
self.tab_width self.tab_width
} }
/// Set tab width (number of spaces between tab stops) /// Set tab width (number of spaces between tab stops).
pub fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { pub fn set_tab_width(&mut self, tab_width: u16) {
// A tab width of 0 is not allowed
if tab_width == 0 { if tab_width == 0 {
return; return;
} }
if tab_width != self.tab_width { if tab_width != self.tab_width {
self.tab_width = tab_width; self.tab_width = tab_width;
// Shaping must be reset when tab width is changed self.dirty |= DirtyFlags::TAB_SHAPE | DirtyFlags::RELAYOUT;
for line in &mut self.lines {
if line.shape_opt().is_some() && line.text().contains('\t') {
line.reset_shaping();
}
}
self.redraw = true; self.redraw = true;
self.shape_until_scroll(font_system, false);
} }
} }
@ -768,42 +803,35 @@ impl Buffer {
(self.width_opt, self.height_opt) (self.width_opt, self.height_opt)
} }
/// Set the current buffer dimensions /// Set the current buffer dimensions.
pub fn set_size( pub fn set_size(&mut self, width_opt: Option<f32>, height_opt: Option<f32>) {
&mut self, let width_clamped = width_opt.map(|v| v.max(0.0));
font_system: &mut FontSystem, let height_clamped = height_opt.map(|v| v.max(0.0));
width_opt: Option<f32>, if width_clamped != self.width_opt {
height_opt: Option<f32>, self.width_opt = width_clamped;
) { self.dirty |= DirtyFlags::RELAYOUT;
self.set_metrics_and_size(font_system, self.metrics, width_opt, height_opt); 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 /// # Panics
/// ///
/// Will panic if `metrics.font_size` is zero. /// Will panic if `metrics.font_size` is zero.
pub fn set_metrics_and_size( pub fn set_metrics_and_size(
&mut self, &mut self,
font_system: &mut FontSystem,
metrics: Metrics, metrics: Metrics,
width_opt: Option<f32>, width_opt: Option<f32>,
height_opt: Option<f32>, height_opt: Option<f32>,
) { ) {
let clamped_width_opt = width_opt.map(|width| width.max(0.0)); self.set_metrics(metrics);
let clamped_height_opt = height_opt.map(|height| height.max(0.0)); self.set_size(width_opt, height_opt);
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);
}
} }
/// Get the current scroll location /// Get the current scroll location
@ -815,45 +843,73 @@ impl Buffer {
pub fn set_scroll(&mut self, scroll: Scroll) { pub fn set_scroll(&mut self, scroll: Scroll) {
if scroll != self.scroll { if scroll != self.scroll {
self.scroll = scroll; self.scroll = scroll;
self.dirty |= DirtyFlags::SCROLL;
self.redraw = true; self.redraw = true;
} }
} }
/// Set text of buffer, using provided attributes for each line by default /// Internal: set text of buffer, reusing existing line allocations.
pub fn set_text( ///
/// Does NOT call `shape_until_scroll` — the caller is responsible for that.
fn set_text_impl(
&mut self, &mut self,
font_system: &mut FontSystem,
text: &str, text: &str,
attrs: &Attrs, attrs: &Attrs,
shaping: Shaping, shaping: Shaping,
alignment: Option<Align>, alignment: Option<Align>,
) { ) {
self.lines.clear(); let mut line_count = 0;
for (range, ending) in LineIter::new(text) { for (range, ending) in LineIter::new(text) {
self.lines.push(BufferLine::new( let line_text = &text[range];
&text[range], if line_count < self.lines.len() {
ending, // Reuse existing line: reclaim String/AttrsList allocations
AttrsList::new(attrs), let mut reused_text = self.lines[line_count].reclaim_text();
shaping, 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 // Ensure there is an ending line with no line ending.
if self // When no lines were produced (empty text), unwrap_or_default() returns
.lines // LineEnding::Lf (the Default), which is != None, so we add an empty line.
.last() let last_ending = if line_count > 0 {
.map(|line| line.ending()) self.lines[line_count - 1].ending()
.unwrap_or_default() } else {
!= LineEnding::None LineEnding::default()
{ };
self.lines.push(BufferLine::new( if last_ending != LineEnding::None {
"", if line_count < self.lines.len() {
LineEnding::None, let reused_text = self.lines[line_count].reclaim_text();
AttrsList::new(attrs), let reused_attrs = self.lines[line_count].reclaim_attrs().reset(attrs);
shaping, 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() { if alignment.is_some() {
self.lines.iter_mut().for_each(|line| { self.lines.iter_mut().for_each(|line| {
line.set_align(alignment); line.set_align(alignment);
@ -861,30 +917,26 @@ impl Buffer {
} }
self.scroll = Scroll::default(); 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) /// Set text of buffer, using provided attributes for each line by default.
/// pub fn set_text(
/// ``` &mut self,
/// # use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping}; text: &str,
/// # let mut font_system = FontSystem::new(); attrs: &Attrs,
/// let mut buffer = Buffer::new_empty(Metrics::new(32.0, 44.0)); shaping: Shaping,
/// let attrs = Attrs::new().family(Family::Serif); alignment: Option<Align>,
/// buffer.set_rich_text( ) {
/// &mut font_system, self.set_text_impl(text, attrs, shaping, alignment);
/// [ self.dirty |= DirtyFlags::TEXT_SET;
/// ("hello, ", attrs.clone()), self.redraw = true;
/// ("cosmic\ntext", attrs.clone().family(Family::Monospace)), }
/// ],
/// &attrs, /// Internal: set rich text of buffer, reusing existing line allocations.
/// Shaping::Advanced, ///
/// None, /// Does NOT call `shape_until_scroll` — the caller is responsible for that.
/// ); fn set_rich_text_impl<'r, 's, I>(
/// ```
pub fn set_rich_text<'r, 's, I>(
&mut self, &mut self,
font_system: &mut FontSystem,
spans: I, spans: I,
default_attrs: &Attrs, default_attrs: &Attrs,
shaping: Shaping, shaping: Shaping,
@ -1017,8 +1069,37 @@ impl Buffer {
}); });
self.scroll = Scroll::default(); 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 /// True if a redraw is needed
@ -1031,12 +1112,24 @@ impl Buffer {
self.redraw = redraw; 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<'_> { pub fn layout_runs(&self) -> LayoutRunIter<'_> {
LayoutRunIter::new(self) 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> { pub fn hit(&self, x: f32, y: f32) -> Option<Cursor> {
#[cfg(all(feature = "std", not(target_arch = "wasm32")))] #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
let instant = std::time::Instant::now(); let instant = std::time::Instant::now();
@ -1478,10 +1571,12 @@ impl Buffer {
Some((cursor, cursor_x_opt)) Some((cursor, cursor_x_opt))
} }
/// Draw the buffer /// Draw the buffer.
///
/// Automatically resolves any pending dirty state before drawing.
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
pub fn draw<F>( pub fn draw<F>(
&self, &mut self,
font_system: &mut FontSystem, font_system: &mut FontSystem,
cache: &mut crate::SwashCache, cache: &mut crate::SwashCache,
color: Color, color: Color,
@ -1489,15 +1584,32 @@ impl Buffer {
) where ) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
self.shape_until_scroll(font_system, false);
let mut renderer = crate::LegacyRenderer { let mut renderer = crate::LegacyRenderer {
font_system, font_system,
cache, cache,
callback, 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 run in self.layout_runs() {
for glyph in run.glyphs { for glyph in run.glyphs {
let physical_glyph = glyph.physical((0., run.line_y), 1.0); 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_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 /// Shape the provided line index and return the result
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> { pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
self.inner.line_shape(self.font_system, line_i) 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) self.inner.line_layout(self.font_system, line_i)
} }
/// Set the current [`Metrics`] /// Set the current [`Metrics`].
/// ///
/// # Panics /// # Panics
/// ///
/// Will panic if `metrics.font_size` is zero. /// Will panic if `metrics.font_size` is zero.
pub fn set_metrics(&mut self, metrics: Metrics) { 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) { 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) { 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>) { 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 /// # Panics
/// ///
@ -1568,15 +1680,22 @@ impl BorrowedWithFontSystem<'_, Buffer> {
height_opt: Option<f32>, height_opt: Option<f32>,
) { ) {
self.inner 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) { 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( pub fn set_text(
&mut self, &mut self,
text: &str, text: &str,
@ -1584,28 +1703,10 @@ impl BorrowedWithFontSystem<'_, Buffer> {
shaping: Shaping, shaping: Shaping,
alignment: Option<Align>, alignment: Option<Align>,
) { ) {
self.inner self.inner.set_text(text, attrs, shaping, alignment);
.set_text(self.font_system, text, attrs, shaping, alignment);
} }
/// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) /// 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>( pub fn set_rich_text<'r, 's, I>(
&mut self, &mut self,
spans: I, spans: I,
@ -1616,7 +1717,30 @@ impl BorrowedWithFontSystem<'_, Buffer> {
I: IntoIterator<Item = (&'s str, Attrs<'r>)>, I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
{ {
self.inner 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`] /// Apply a [`Motion`] to a [`Cursor`]
@ -1630,7 +1754,9 @@ impl BorrowedWithFontSystem<'_, Buffer> {
.cursor_motion(self.font_system, cursor, cursor_x_opt, motion) .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")] #[cfg(feature = "swash")]
pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, color: Color, f: F) pub fn draw<F>(&mut self, cache: &mut crate::SwashCache, color: Color, f: F)
where where

View file

@ -47,10 +47,12 @@ impl<'buffer> Editor<'buffer> {
} }
/// Draw the editor /// Draw the editor
///
/// Automatically resolves any pending dirty state before drawing.
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn draw<F>( pub fn draw<F>(
&self, &mut self,
font_system: &mut FontSystem, font_system: &mut FontSystem,
cache: &mut crate::SwashCache, cache: &mut crate::SwashCache,
text_color: Color, text_color: Color,
@ -61,6 +63,7 @@ impl<'buffer> Editor<'buffer> {
) where ) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, false));
let mut renderer = crate::LegacyRenderer { let mut renderer = crate::LegacyRenderer {
font_system, font_system,
cache, 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>( pub fn render<R: Renderer>(
&self, &self,
renderer: &mut R, renderer: &mut R,
@ -207,8 +214,8 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
self.with_buffer(super::super::buffer::Buffer::tab_width) self.with_buffer(super::super::buffer::Buffer::tab_width)
} }
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { fn set_tab_width(&mut self, tab_width: u16) {
self.with_buffer_mut(|buffer| buffer.set_tab_width(font_system, tab_width)); self.with_buffer_mut(|buffer| buffer.set_tab_width(tab_width));
} }
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { 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 } => { Action::Click { x, y } => {
self.set_selection(Selection::None); 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 { if new_cursor != self.cursor {
self.cursor = new_cursor; self.cursor = new_cursor;
self.with_buffer_mut(|buffer| buffer.set_redraw(true)); self.with_buffer_mut(|buffer| buffer.set_redraw(true));
@ -775,8 +784,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
Action::DoubleClick { x, y } => { Action::DoubleClick { x, y } => {
self.set_selection(Selection::None); 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 { if new_cursor != self.cursor {
self.cursor = new_cursor; self.cursor = new_cursor;
self.with_buffer_mut(|buffer| buffer.set_redraw(true)); self.with_buffer_mut(|buffer| buffer.set_redraw(true));
@ -788,8 +799,10 @@ impl<'buffer> Edit<'buffer> for Editor<'buffer> {
Action::TripleClick { x, y } => { Action::TripleClick { x, y } => {
self.set_selection(Selection::None); 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 { if new_cursor != self.cursor {
self.cursor = new_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)); 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 { if new_cursor != self.cursor {
self.cursor = new_cursor; self.cursor = new_cursor;
self.with_buffer_mut(|buffer| buffer.set_redraw(true)); self.with_buffer_mut(|buffer| buffer.set_redraw(true));

View file

@ -292,7 +292,7 @@ pub trait Edit<'buffer> {
fn tab_width(&self) -> u16; fn tab_width(&self) -> u16;
/// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored /// 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 /// Shape lines until scroll, after adjusting scroll if the cursor moved
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool); 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 /// 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) { 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 /// 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")] #[cfg(feature = "std")]
pub fn load_text<P: AsRef<Path>>( pub fn load_text<P: AsRef<Path>>(
&mut self, &mut self,
font_system: &mut FontSystem, _font_system: &mut FontSystem,
path: P, path: P,
mut attrs: crate::Attrs, mut attrs: crate::Attrs,
) -> io::Result<()> { ) -> io::Result<()> {
@ -125,7 +125,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
// Clear buffer first (allows sane handling of non-existant files) // Clear buffer first (allows sane handling of non-existant files)
self.editor.with_buffer_mut(|buffer| { 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 // Update syntax based on file name
@ -147,7 +147,7 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
// Set text // Set text
let text = fs::read_to_string(path)?; let text = fs::read_to_string(path)?;
self.editor.with_buffer_mut(|buffer| { 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(()) Ok(())
@ -218,19 +218,32 @@ impl<'syntax_system, 'buffer> SyntaxEditor<'syntax_system, 'buffer> {
} }
/// Draw the editor /// Draw the editor
///
/// Automatically resolves any pending dirty state before drawing.
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F) pub fn draw<F>(
where &mut self,
font_system: &mut FontSystem,
cache: &mut crate::SwashCache,
callback: F,
) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
let mut renderer = crate::LegacyRenderer { self.editor.draw(
font_system, font_system,
cache, cache,
self.foreground_color(),
self.cursor_color(),
self.selection_color(),
self.foreground_color(),
callback, 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) { pub fn render<R: Renderer>(&self, renderer: &mut R) {
let size = self.with_buffer(|buffer| buffer.size()); let size = self.with_buffer(|buffer| buffer.size());
if let Some(width) = size.0 { if let Some(width) = size.0 {
@ -285,8 +298,8 @@ impl<'buffer> Edit<'buffer> for SyntaxEditor<'_, 'buffer> {
self.editor.tab_width() self.editor.tab_width()
} }
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { fn set_tab_width(&mut self, tab_width: u16) {
self.editor.set_tab_width(font_system, tab_width); self.editor.set_tab_width(tab_width);
} }
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { 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); self.changed = eval_changed(&self.commands, self.save_pivot);
} }
/// Draw the editor.
///
/// Automatically resolves any pending dirty state before drawing.
#[cfg(feature = "swash")] #[cfg(feature = "swash")]
pub fn draw<F>(&self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, callback: F) pub fn draw<F>(
where &mut self,
font_system: &mut FontSystem,
cache: &mut crate::SwashCache,
callback: F,
) where
F: FnMut(i32, i32, u32, u32, Color), F: FnMut(i32, i32, u32, u32, Color),
{ {
self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, false));
let mut renderer = crate::LegacyRenderer { let mut renderer = crate::LegacyRenderer {
font_system, font_system,
cache, cache,
@ -315,6 +323,10 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> {
self.render(&mut renderer); 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) { pub fn render<R: Renderer>(&self, renderer: &mut R) {
let background_color = self.background_color(); let background_color = self.background_color();
let foreground_color = self.foreground_color(); let foreground_color = self.foreground_color();
@ -553,8 +565,8 @@ impl<'buffer> Edit<'buffer> for ViEditor<'_, 'buffer> {
self.editor.tab_width() self.editor.tab_width()
} }
fn set_tab_width(&mut self, font_system: &mut FontSystem, tab_width: u16) { fn set_tab_width(&mut self, tab_width: u16) {
self.editor.set_tab_width(font_system, tab_width); self.editor.set_tab_width(tab_width);
} }
fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { 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 //! // Borrow buffer together with the font system for more convenient method calls
//! let mut buffer = buffer.borrow_with(&mut font_system); //! 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 //! // Attributes indicate what font to choose
//! let attrs = Attrs::new(); //! 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); //! buffer.set_text("Hello, Rust! 🦀\n", &attrs, Shaping::Advanced, None);
//! //!
//! // Perform shaping as desired
//! buffer.shape_until_scroll(true);
//!
//! // Inspect the output runs //! // Inspect the output runs
//! for run in buffer.layout_runs() { //! for run in buffer.layout_runs() {
//! for glyph in run.glyphs.iter() { //! for glyph in run.glyphs.iter() {

View file

@ -1262,6 +1262,11 @@ impl ShapeLine {
shaping: Shaping, shaping: Shaping,
tab_width: u16, 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); 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. // 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, self.alignment,
); );
} }
buffer.shape_until_scroll(true);
// Black // Black
let text_color = Color::rgb(0x00, 0x00, 0x00); let text_color = Color::rgb(0x00, 0x00, 0x00);

View file

@ -28,7 +28,6 @@ fn empty_lines_use_span_metrics() {
None, None,
); );
buffer.set_size(Some(500.0), Some(500.0)); 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(); 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); let mut buffer = buffer.borrow_with(&mut font_system);
buffer.set_text("|>", &Attrs::new(), Shaping::Advanced, None); buffer.set_text("|>", &Attrs::new(), Shaping::Advanced, None);
buffer.shape_until_scroll(false); let _ = buffer.layout_runs();
let line = &buffer.lines[0]; let line = &buffer.lines[0];
let shape = line.shape_opt().expect("ShapeLine not found"); let shape = line.shape_opt().expect("ShapeLine not found");
@ -105,7 +105,7 @@ fn test_ligature_segmentation() {
// Test -> (Arrow), which is a common ligature. // Test -> (Arrow), which is a common ligature.
buffer.set_text("->", &Attrs::new(), Shaping::Advanced, None); buffer.set_text("->", &Attrs::new(), Shaping::Advanced, None);
buffer.shape_until_scroll(false); let _ = buffer.layout_runs();
let line = &buffer.lines[0]; let line = &buffer.lines[0];
let shape = line.shape_opt().expect("ShapeLine not found"); let shape = line.shape_opt().expect("ShapeLine not found");
@ -118,7 +118,7 @@ fn test_ligature_segmentation() {
// Test != // Test !=
buffer.set_text("!=", &Attrs::new(), Shaping::Advanced, None); buffer.set_text("!=", &Attrs::new(), Shaping::Advanced, None);
buffer.shape_until_scroll(false); let _ = buffer.layout_runs();
let line = &buffer.lines[0]; let line = &buffer.lines[0];
let shape = line.shape_opt().expect("ShapeLine not found"); let shape = line.shape_opt().expect("ShapeLine not found");
// Inter has a contextual alternate for != too. // Inter has a contextual alternate for != too.
@ -131,7 +131,7 @@ fn test_ligature_segmentation() {
// Test ++ // Test ++
buffer.set_text("++", &Attrs::new(), Shaping::Advanced, None); buffer.set_text("++", &Attrs::new(), Shaping::Advanced, None);
buffer.shape_until_scroll(false); let _ = buffer.layout_runs();
let line = &buffer.lines[0]; let line = &buffer.lines[0];
let shape = line.shape_opt().expect("ShapeLine not found"); let shape = line.shape_opt().expect("ShapeLine not found");
// Inter does not have a ++ ligature. // 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); 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_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)); buffer.set_size(Some(50.0), Some(1000.0));
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);
// Perform shaping as desired
buffer.shape_until_scroll(false);
let empty_lines = buffer.layout_runs().filter(|x| x.line_w == 0.).count(); 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(); 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); let mut buffer = buffer.borrow_with(&mut font_system);
buffer.set_wrap(Wrap::WordOrGlyph); 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_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 = buffer
.layout_runs()
let measured_size = measure(&buffer); .fold(0.0f32, |width, run| width.max(run.line_w));
assert!( assert!(
measured_size <= buffer.size().0.unwrap_or(0.0), measured_size <= buffer.size().0.unwrap_or(0.0),
@ -29,9 +29,3 @@ fn wrap_word_fallback() {
buffer.size().0.unwrap_or(0.0) 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))
}