Compare commits
No commits in common. "63072bbe29a1657d82cd3deb5db45070404ec7a1" and "1abcf67e179d5cde17684c8c1606f5ee4ca418c8" have entirely different histories.
63072bbe29
...
1abcf67e17
5 changed files with 16 additions and 182 deletions
26
CHANGELOG.md
26
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/),
|
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).
|
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
|
## [0.18.2] - 2026-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-text"
|
name = "cosmic-text"
|
||||||
description = "Pure Rust multi-line text handling"
|
description = "Pure Rust multi-line text handling"
|
||||||
version = "0.19.0"
|
version = "0.18.2"
|
||||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
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 }
|
cosmic_undo_2 = { version = "0.2.0", optional = true }
|
||||||
fontdb = { version = "0.23", default-features = false }
|
fontdb = { version = "0.23", default-features = false }
|
||||||
harfrust = { version = "0.5.0", 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 }
|
libm = { version = "0.2.16", optional = true }
|
||||||
linebender_resource_handle = { version = "0.1.1", default-features = false }
|
linebender_resource_handle = { version = "0.1.1", default-features = false }
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
|
|
@ -30,8 +30,6 @@ sys-locale = { version = "0.3.2", optional = true }
|
||||||
unicode-linebreak = "0.1.5"
|
unicode-linebreak = "0.1.5"
|
||||||
unicode-script = "0.5.8"
|
unicode-script = "0.5.8"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
# Yoda: EAW-aware cell width for monospace/terminal rendering.
|
|
||||||
unicode-width = "0.2"
|
|
||||||
|
|
||||||
[dependencies.swash]
|
[dependencies.swash]
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
|
@ -48,7 +46,7 @@ features = ["hardcoded-data"]
|
||||||
default = ["std", "swash", "fontconfig"]
|
default = ["std", "swash", "fontconfig"]
|
||||||
fontconfig = ["fontdb/fontconfig", "std"]
|
fontconfig = ["fontdb/fontconfig", "std"]
|
||||||
monospace_fallback = []
|
monospace_fallback = []
|
||||||
no_std = ["hashbrown", "dep:libm", "skrifa/libm", "core_maths", "swash?/libm"]
|
no_std = ["hashbrown", "dep:libm", "skrifa/libm", "core_maths"]
|
||||||
peniko = []
|
peniko = []
|
||||||
shape-run-cache = []
|
shape-run-cache = []
|
||||||
std = [
|
std = [
|
||||||
|
|
|
||||||
|
|
@ -573,13 +573,6 @@ impl Buffer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let metrics = self.metrics;
|
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;
|
let old_scroll = self.scroll;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -1241,21 +1234,6 @@ impl Buffer {
|
||||||
new_cursor_opt
|
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<bool> {
|
|
||||||
self.lines.get(line)?.shape_opt().map(|shape| shape.rtl)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a [`Motion`] to a [`Cursor`]
|
/// Apply a [`Motion`] to a [`Cursor`]
|
||||||
pub fn cursor_motion(
|
pub fn cursor_motion(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
|
||||||
135
src/shape.rs
135
src/shape.rs
|
|
@ -5,8 +5,8 @@
|
||||||
use crate::fallback::FontFallbackIter;
|
use crate::fallback::FontFallbackIter;
|
||||||
use crate::{
|
use crate::{
|
||||||
math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, DecorationSpan,
|
math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, DecorationSpan,
|
||||||
Ellipsize, EllipsizeHeightLimit, Family, Font, FontSystem, GlyphDecorationData, Hinting,
|
Ellipsize, EllipsizeHeightLimit, Font, FontSystem, GlyphDecorationData, Hinting, LayoutGlyph,
|
||||||
LayoutGlyph, LayoutLine, Metrics, Wrap,
|
LayoutLine, Metrics, Wrap,
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::{format, vec, vec::Vec};
|
use alloc::{format, vec, vec::Vec};
|
||||||
|
|
@ -237,8 +237,6 @@ fn shape_fallback(
|
||||||
metadata: attrs.metadata,
|
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, &attrs),
|
||||||
metrics_opt: attrs.metrics_opt.map(Into::into),
|
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.
|
// Restore the buffer to save an allocation.
|
||||||
scratch.harfrust_buffer = Some(glyph_buffer.clear());
|
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 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<ShapeGlyph>,
|
|
||||||
font: &Font,
|
|
||||||
line: &str,
|
|
||||||
attrs_list: &AttrsList,
|
|
||||||
start_run: usize,
|
|
||||||
end_run: usize,
|
|
||||||
) {
|
|
||||||
let font_id = font.id();
|
let font_id = font.id();
|
||||||
let font_monospace_em_width = font.monospace_em_width();
|
let font_monospace_em_width = font.monospace_em_width();
|
||||||
let swash_font = font.as_swash();
|
let swash_font = font.as_swash();
|
||||||
|
|
@ -600,17 +513,8 @@ fn shape_skip_glyphs(
|
||||||
.map(|(chr_idx, codepoint)| {
|
.map(|(chr_idx, codepoint)| {
|
||||||
let glyph_id = charmap.map(codepoint);
|
let glyph_id = charmap.map(codepoint);
|
||||||
let x_advance = glyph_metrics.advance_width(glyph_id)
|
let x_advance = glyph_metrics.advance_width(glyph_id)
|
||||||
+ attrs_list
|
+ attrs.letter_spacing_opt.map_or(0.0, |spacing| spacing.0);
|
||||||
.get_span(start_run + chr_idx)
|
|
||||||
.letter_spacing_opt
|
|
||||||
.map_or(0.0, |spacing| spacing.0);
|
|
||||||
let attrs = attrs_list.get_span(start_run + chr_idx);
|
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 {
|
ShapeGlyph {
|
||||||
start: chr_idx + start_run,
|
start: chr_idx + start_run,
|
||||||
|
|
@ -627,9 +531,12 @@ fn shape_skip_glyphs(
|
||||||
glyph_id,
|
glyph_id,
|
||||||
color_opt: attrs.color_opt,
|
color_opt: attrs.color_opt,
|
||||||
metadata: attrs.metadata,
|
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),
|
metrics_opt: attrs.metrics_opt.map(Into::into),
|
||||||
terminal_cells,
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -666,14 +573,6 @@ pub struct ShapeGlyph {
|
||||||
pub metadata: usize,
|
pub metadata: usize,
|
||||||
pub cache_key_flags: CacheKeyFlags,
|
pub cache_key_flags: CacheKeyFlags,
|
||||||
pub metrics_opt: Option<Metrics>,
|
pub metrics_opt: Option<Metrics>,
|
||||||
/// 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 {
|
impl ShapeGlyph {
|
||||||
|
|
@ -2933,21 +2832,9 @@ impl ShapeLine {
|
||||||
0.0
|
0.0
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if let Some(mono_width) = match_mono_width {
|
if let Some(match_em_width) = match_mono_em_width {
|
||||||
// Yoda: use Unicode East Asian Width, stored at
|
// Round to nearest monospace width
|
||||||
// shape time, as the authoritative cell count.
|
x_advance = ((x_advance / match_em_width).round()) * match_em_width;
|
||||||
// `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 hinting == Hinting::Enabled {
|
if hinting == Hinting::Enabled {
|
||||||
x_advance = x_advance.round();
|
x_advance = x_advance.round();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::boxed::Box;
|
use alloc::vec::Vec;
|
||||||
#[cfg(feature = "no_std")]
|
|
||||||
use core_maths::CoreFloat;
|
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use swash::scale::{image::Content, ScaleContext};
|
use swash::scale::{image::Content, ScaleContext};
|
||||||
use swash::scale::{Render, Source, StrikeWith};
|
use swash::scale::{Render, Source, StrikeWith};
|
||||||
|
|
@ -47,7 +44,7 @@ fn swash_image(
|
||||||
// in a real renderer
|
// in a real renderer
|
||||||
let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
|
let offset = if cache_key.flags.contains(CacheKeyFlags::PIXEL_FONT) {
|
||||||
Vector::new(
|
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(),
|
cache_key.y_bin.as_float().round(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue