Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 152 additions & 18 deletions crates/gpui/src/text_system/line_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,7 @@ impl LineLayoutCache {
.layout_line(&text, font_size, runs);

if let Some(force_width) = force_width {
let mut glyph_pos = 0;
for run in layout.runs.iter_mut() {
for glyph in run.glyphs.iter_mut() {
if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
glyph.position.x = glyph_pos * force_width;
}
glyph_pos += 1;
}
}
apply_force_width_to_layout(&mut layout, force_width);
}

let key = Arc::new(CacheKey {
Expand Down Expand Up @@ -767,15 +759,7 @@ impl LineLayoutCache {
.layout_line(&text, font_size, runs);

if let Some(force_width) = force_width {
let mut glyph_pos = 0;
for run in layout.runs.iter_mut() {
for glyph in run.glyphs.iter_mut() {
if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
glyph.position.x = glyph_pos * force_width;
}
glyph_pos += 1;
}
}
apply_force_width_to_layout(&mut layout, force_width);
}

let key = Arc::new(HashedCacheKey {
Expand All @@ -795,6 +779,36 @@ impl LineLayoutCache {
}
}

// Combining marks (e.g. Thai vowel signs, Arabic diacritics) are shaped by
// HarfBuzz at the same x position as their base character. The force-width
// loop must not advance the cell counter for these zero-advance glyphs,
// otherwise they get displaced into the next cell. We detect them by checking
// whether shaped x has advanced by at least half a cell beyond the last base.
fn apply_force_width_to_layout(layout: &mut LineLayout, force_width: Pixels) {
let mut glyph_pos: usize = 0;
// NEG_INFINITY ensures the first glyph is always classified as a base.
let mut last_base_shaped_x = px(f32::NEG_INFINITY);
let mut last_base_actual_x = px(0.);

for run in layout.runs.iter_mut() {
for glyph in run.glyphs.iter_mut() {
let shaped_x = glyph.position.x;

if shaped_x > last_base_shaped_x + force_width * 0.5 {
let forced_x = glyph_pos * force_width;
if (shaped_x - forced_x).abs() > px(1.) {
glyph.position.x = forced_x;
}
last_base_shaped_x = shaped_x;
last_base_actual_x = glyph.position.x;
glyph_pos += 1;
} else {
glyph.position.x = last_base_actual_x + (shaped_x - last_base_shaped_x);
}
}
}
}

/// A run of text with a single font.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[expect(missing_docs)]
Expand Down Expand Up @@ -942,3 +956,123 @@ impl AsCacheKeyRef for CacheKeyRef<'_> {
*self
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::GlyphId;

fn glyph_at(x: f32, index: usize) -> ShapedGlyph {
ShapedGlyph {
id: GlyphId(0),
position: point(px(x), px(0.)),
index,
is_emoji: false,
}
}

fn make_layout(glyphs: Vec<ShapedGlyph>) -> LineLayout {
LineLayout {
font_size: px(16.),
width: px(100.),
ascent: px(12.),
descent: px(4.),
runs: vec![ShapedRun {
font_id: FontId(0),
glyphs,
}],
len: 0,
}
}

fn glyph_x_positions(layout: &LineLayout) -> Vec<f32> {
layout.runs[0]
.glyphs
.iter()
.map(|g| f32::from(g.position.x))
.collect()
}

#[test]
fn test_force_width_latin_unchanged() {
let cell_width = px(8.);
let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(8., 1), glyph_at(16., 2)]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 8., 16.]);
}

#[test]
fn test_force_width_combining_marks_not_advanced() {
let cell_width = px(8.);
// Simulates Thai "กี" — base consonant at x=0, combining vowel also at x=0
let mut layout = make_layout(vec![
glyph_at(0., 0), // ก (base)
glyph_at(0., 3), // ี (combining mark, same x)
]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0.]);
}

#[test]
fn test_force_width_base_after_combining_mark() {
let cell_width = px(8.);
let mut layout = make_layout(vec![glyph_at(0., 0), glyph_at(0., 3), glyph_at(8., 6)]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0., 8.]);
}

#[test]
fn test_force_width_multiple_combining_marks() {
let cell_width = px(8.);
// Simulates "ก้" — base + vowel + tone mark (two combining marks stacked)
let mut layout = make_layout(vec![
glyph_at(0., 0), // ก (base)
glyph_at(0., 3), // vowel (combining)
glyph_at(0., 6), // tone mark (combining)
glyph_at(8., 9), // next base
]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0., 0., 0., 8.]);
}

#[test]
fn test_force_width_corrects_drifted_base_positions() {
let cell_width = px(8.);
// Font metrics don't perfectly match cell grid — glyphs drift >1px from cell boundary
let mut layout = make_layout(vec![
glyph_at(0.5, 0), // within 1px tolerance, kept as-is
glyph_at(10.2, 1), // >1px off from 8.0, corrected
glyph_at(19.8, 2), // >1px off from 16.0, corrected
]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0.5, 8., 16.]);
}

#[test]
fn test_force_width_combining_mark_after_within_tolerance_base() {
let cell_width = px(8.);
// Base glyph is within 1px of grid so it keeps its shaped position.
// The combining mark must align to the base's actual position, not the grid slot.
let mut layout = make_layout(vec![glyph_at(0.5, 0), glyph_at(0.5, 3)]);

apply_force_width_to_layout(&mut layout, cell_width);

let positions = glyph_x_positions(&layout);
assert_eq!(positions, vec![0.5, 0.5]);
}
}
Loading