diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a462b..98e31a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,32 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.19.0] - 2026-04-22 - -### Fixed - -- Empty lines use span metrics: https://github.com/pop-os/cosmic-text/pull/479 -- Match variable fonts using wght axis: https://github.com/pop-os/cosmic-text/pull/486 -- Used normalized\_coords for swash scaler: https://github.com/pop-os/cosmic-text/pull/488 -- Fix highlight for editor: https://github.com/pop-os/cosmic-text/pull/491 -- Optimize shape\_until\_scroll with large line count: https://github.com/pop-os/cosmic-text/pull/490 -- Fix shape\_until\_scroll if a buffer\_line is modified: https://github.com/pop-os/cosmic-text/pull/494 -- Remove 1px horizontal offset on pixel fonts: https://github.com/pop-os/cosmic-text/pull/495 -- Make the swash feature compile with no\_std: https://github.com/pop-os/cosmic-text/pull/492 -- Fallback to a default font in basic shaping mode: https://github.com/pop-os/cosmic-text/pull/498 -- Clamp scroll.line to a valid range: https://github.com/pop-os/cosmic-text/pull/501 - -### Added - -- Text decoration: https://github.com/pop-os/cosmic-text/pull/480 -- Implement layout\_runs for BufferLine: https://github.com/pop-os/cosmic-text/pull/484 -- Add cursor\_position and is\_rtl methods to buffer: https://github.com/pop-os/cosmic-text/pull/496 - -### Changed - -- Buffer setter methods are now lazy: https://github.com/pop-os/cosmic-text/pull/483 -- Update hashbrown: https://github.com/pop-os/cosmic-text/pull/502 - ## [0.18.2] - 2026-02-20 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 227b603..dedb16c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-text" description = "Pure Rust multi-line text handling" -version = "0.19.0" +version = "0.18.2" authors = ["Jeremy Soller "] edition = "2021" license = "MIT OR Apache-2.0" @@ -15,7 +15,7 @@ core_maths = { version = "0.1.1", optional = true } cosmic_undo_2 = { version = "0.2.0", optional = true } fontdb = { version = "0.23", default-features = false } harfrust = { version = "0.5.0", default-features = false } -hashbrown = { version = "0.17", optional = true, default-features = false } +hashbrown = { version = "0.16", optional = true, default-features = false } libm = { version = "0.2.16", optional = true } linebender_resource_handle = { version = "0.1.1", default-features = false } log = "0.4.29" @@ -30,8 +30,6 @@ sys-locale = { version = "0.3.2", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.8" unicode-segmentation = "1.12.0" -# Yoda: EAW-aware cell width for monospace/terminal rendering. -unicode-width = "0.2" [dependencies.swash] version = "0.2.6" @@ -48,7 +46,7 @@ features = ["hardcoded-data"] default = ["std", "swash", "fontconfig"] fontconfig = ["fontdb/fontconfig", "std"] monospace_fallback = [] -no_std = ["hashbrown", "dep:libm", "skrifa/libm", "core_maths", "swash?/libm"] +no_std = ["hashbrown", "dep:libm", "skrifa/libm", "core_maths"] peniko = [] shape-run-cache = [] std = [ diff --git a/src/buffer.rs b/src/buffer.rs index f8fa5ff..9d8b49a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -573,13 +573,6 @@ impl Buffer { return; } let metrics = self.metrics; - - // Clamp scroll.line to valid range (lines may have been removed by editing) - if self.scroll.line >= self.lines.len() { - self.scroll.line = self.lines.len().saturating_sub(1); - self.scroll.vertical = 0.0; - } - let old_scroll = self.scroll; loop { @@ -1241,21 +1234,6 @@ impl Buffer { new_cursor_opt } - /// Returns the visual (x, y) position of a cursor within the buffer. - /// y is the top of the line containing the cursor. - /// This is a convenience wrapper around [`LayoutRun::cursor_position`]. - pub fn cursor_position(&self, cursor: &Cursor) -> Option<(f32, f32)> { - self.layout_runs() - .filter(|run| run.line_i == cursor.line) - .find_map(|run| run.cursor_position(cursor).map(|x| (x, run.line_top))) - } - - /// Returns if the text direction for a given line is RTL - /// Returns `None` if the line doesn't exist or hasn't been shaped yet. - pub fn is_rtl(&self, line: usize) -> Option { - self.lines.get(line)?.shape_opt().map(|shape| shape.rtl) - } - /// Apply a [`Motion`] to a [`Cursor`] pub fn cursor_motion( &mut self, diff --git a/src/shape.rs b/src/shape.rs index 5e76c73..5e187af 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -5,8 +5,8 @@ use crate::fallback::FontFallbackIter; use crate::{ math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, DecorationSpan, - Ellipsize, EllipsizeHeightLimit, Family, Font, FontSystem, GlyphDecorationData, Hinting, - LayoutGlyph, LayoutLine, Metrics, Wrap, + Ellipsize, EllipsizeHeightLimit, Font, FontSystem, GlyphDecorationData, Hinting, LayoutGlyph, + LayoutLine, Metrics, Wrap, }; #[cfg(not(feature = "std"))] use alloc::{format, vec, vec::Vec}; @@ -237,8 +237,6 @@ fn shape_fallback( metadata: attrs.metadata, cache_key_flags: override_fake_italic(attrs.cache_key_flags, font, &attrs), metrics_opt: attrs.metrics_opt.map(Into::into), - // Set by the post-loop pass below once `end` is finalized. - terminal_cells: 1, }); } @@ -267,19 +265,6 @@ fn shape_fallback( } } - // Yoda: compute EAW terminal width per glyph now that `end` is finalized. - // We use the cluster's text (byte range into `line`) rather than just the - // first char so ZWJ emoji sequences collapse to a single cluster width. - // `unicode-width` returns 0 for combining marks / variation selectors, 2 - // for CJK & most emoji, 1 for everything else including Arabic. - { - use unicode_width::UnicodeWidthStr; - for glyph in &mut glyphs[glyph_start..] { - let cluster = line.get(glyph.start..glyph.end).unwrap_or(""); - glyph.terminal_cells = UnicodeWidthStr::width(cluster).min(2) as u8; - } - } - // Restore the buffer to save an allocation. scratch.harfrust_buffer = Some(glyph_buffer.clear()); @@ -511,78 +496,6 @@ fn shape_skip( ); let font = font_iter.next().expect("no default font found"); - let glyph_start = glyphs.len(); - - shape_skip_glyphs(glyphs, &font, line, attrs_list, start_run, end_run); - - // If any glyphs are missing and the user has specified a font, - // fall back to a default font (SansSerif or Monospace) - if matches!(attrs.family, Family::Name(_)) - && glyphs[glyph_start..].iter().any(|g| g.glyph_id == 0) - { - let is_mono = font_system - .db() - .face(font.id()) - .is_some_and(|face| face.monospaced); - let fb_family = if is_mono { - Family::Monospace - } else { - Family::SansSerif - }; - let fb_attrs = Attrs::new() - .family(fb_family) - .weight(attrs.weight) - .style(attrs.style) - .stretch(attrs.stretch); - let fb_fonts = font_system.get_font_matches(&fb_attrs); - let fb_families = [&fb_family]; - let mut fb_iter = - FontFallbackIter::new(font_system, &fb_fonts, &fb_families, &[], "", attrs.weight); - - if let Some(fb_font) = fb_iter.next() { - let fb_swash = fb_font.as_swash(); - let fb_charmap = fb_swash.charmap(); - let fb_metrics = fb_swash.metrics(&[]); - let fb_glyph_metrics = fb_swash.glyph_metrics(&[]).scale(1.0); - let fb_scale = f32::from(fb_metrics.units_per_em); - - for glyph in glyphs[glyph_start..].iter_mut() { - if glyph.glyph_id != 0 { - continue; - } - let codepoint = line[glyph.start..glyph.end].chars().next().unwrap_or('\0'); - let glyph_id = fb_charmap.map(codepoint); - if glyph_id != 0 { - let span_attrs = attrs_list.get_span(glyph.start); - glyph.glyph_id = glyph_id; - glyph.font_id = fb_font.id(); - glyph.font_monospace_em_width = fb_font.monospace_em_width(); - glyph.ascent = fb_metrics.ascent / fb_scale; - glyph.descent = fb_metrics.descent / fb_scale; - glyph.x_advance = fb_glyph_metrics.advance_width(glyph_id) - + span_attrs - .letter_spacing_opt - .map_or(0.0, |spacing| spacing.0); - glyph.cache_key_flags = override_fake_italic( - span_attrs.cache_key_flags, - fb_font.as_ref(), - &span_attrs, - ); - } - } - } - } -} - -#[cfg(feature = "swash")] -fn shape_skip_glyphs( - glyphs: &mut Vec, - font: &Font, - line: &str, - attrs_list: &AttrsList, - start_run: usize, - end_run: usize, -) { let font_id = font.id(); let font_monospace_em_width = font.monospace_em_width(); let swash_font = font.as_swash(); @@ -600,17 +513,8 @@ fn shape_skip_glyphs( .map(|(chr_idx, codepoint)| { let glyph_id = charmap.map(codepoint); let x_advance = glyph_metrics.advance_width(glyph_id) - + attrs_list - .get_span(start_run + chr_idx) - .letter_spacing_opt - .map_or(0.0, |spacing| spacing.0); + + attrs.letter_spacing_opt.map_or(0.0, |spacing| spacing.0); let attrs = attrs_list.get_span(start_run + chr_idx); - // Yoda: EAW width for terminal-aware monospace snap (see - // ShapeGlyph.terminal_cells doc). - let terminal_cells = { - use unicode_width::UnicodeWidthChar; - UnicodeWidthChar::width(codepoint).unwrap_or(1).min(2) as u8 - }; ShapeGlyph { start: chr_idx + start_run, @@ -627,9 +531,12 @@ fn shape_skip_glyphs( glyph_id, color_opt: attrs.color_opt, metadata: attrs.metadata, - cache_key_flags: override_fake_italic(attrs.cache_key_flags, font, &attrs), + cache_key_flags: override_fake_italic( + attrs.cache_key_flags, + font.as_ref(), + &attrs, + ), metrics_opt: attrs.metrics_opt.map(Into::into), - terminal_cells, } }), ); @@ -666,14 +573,6 @@ pub struct ShapeGlyph { pub metadata: usize, pub cache_key_flags: CacheKeyFlags, pub metrics_opt: Option, - /// Yoda: Unicode East Asian Width of the source text covered by this glyph, - /// in terminal cells (0, 1 or 2 per `unicode_width::UnicodeWidthStr::width`). - /// Populated at shape time from `line[start..end]`. Consumed by - /// `layout_to_buffer` when `match_mono_width` is Some — wide chars (emoji, - /// CJK, fullwidth forms) snap to 2 cells instead of rounding based on the - /// fallback font's natural glyph advance, which was the upstream bug - /// breaking terminal column alignment. - pub terminal_cells: u8, } impl ShapeGlyph { @@ -2933,21 +2832,9 @@ impl ShapeLine { 0.0 }, ); - if let Some(mono_width) = match_mono_width { - // Yoda: use Unicode East Asian Width, stored at - // shape time, as the authoritative cell count. - // `terminal_cells` is 0 for combining marks / - // variation selectors (they stay 0-advance), - // 2 for CJK & emoji clusters, 1 for everything - // else (including Arabic, Latin, etc.). - // - // This replaces the previous round-to-nearest - // logic which produced variable cell widths - // for fallback glyphs because it depended on - // the glyph's natural advance in the fallback - // font rather than on the source character's - // terminal width spec. - x_advance = f32::from(glyph.terminal_cells) * mono_width; + if let Some(match_em_width) = match_mono_em_width { + // Round to nearest monospace width + x_advance = ((x_advance / match_em_width).round()) * match_em_width; } if hinting == Hinting::Enabled { x_advance = x_advance.round(); diff --git a/src/swash.rs b/src/swash.rs index c644ca1..8ff411a 100644 --- a/src/swash.rs +++ b/src/swash.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] -use alloc::boxed::Box; -#[cfg(feature = "no_std")] -use core_maths::CoreFloat; - +use alloc::vec::Vec; use core::fmt; use swash::scale::{image::Content, ScaleContext}; use swash::scale::{Render, Source, StrikeWith}; @@ -47,7 +44,7 @@ fn swash_image( // in a real renderer let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) { Vector::new( - cache_key.x_bin.as_float().round(), + cache_key.x_bin.as_float().round() + 1.0, cache_key.y_bin.as_float().round(), ) } else {