Write a final Greek sigma in lower-case text layout

Found while testing crates/text/src/layout.rs:1070 (shape_segment calls TextCase::apply_after one char at a time; crates/doc/src/text.rs:124 apply_after)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-22 19:04:10 +02:00
co-authored by Claude Opus 5.5
parent 9ec80bf2c2
commit 83b9f74665
2 changed files with 36 additions and 0 deletions
+14
View File
@@ -1035,6 +1035,18 @@ fn is_emoji(c: char) -> bool {
matches!(c, '\u{1F1E6}'..='\u{1F1FF}' | '\u{1F300}'..='\u{1FAFF}')
}
/// The lower case of the capital sigma at `i`: the final form ς or σ, chosen from the letters
/// around it in the whole text (as lower-casing the whole string would), not from the lone char.
fn lower_sigma(chars: &[char], i: usize) -> char {
// Enough context to step over any realistic run of combining marks next to the sigma.
const CONTEXT: usize = 32;
let before: String = chars.get(i.saturating_sub(CONTEXT)..i).unwrap_or(&[]).iter().collect();
let after: String = chars.iter().skip(i + 1).take(CONTEXT).collect();
let lower = format!("{before}\u{3A3}{after}").to_lowercase();
// Only the sigma's lower case depends on context, so the text before it keeps its length.
lower.get(before.to_lowercase().len()..).and_then(|s| s.chars().next()).unwrap_or('σ')
}
/// One run of chars shaped with a single face and style.
struct Segment<'a> {
start: usize,
@@ -1065,6 +1077,8 @@ fn shape_segment(chars: &[char], seg: &Segment, face: &Arc<Face>, features: &[Fe
for u in c.to_uppercase() {
mapped.push((u, src, true));
}
} else if style.case == TextCase::Lower && *c == 'Σ' {
mapped.push((lower_sigma(chars, src), src, false));
} else {
let mut buf = [0u8; 4];
for u in style.case.apply_after(c.encode_utf8(&mut buf), prev).chars() {
+22
View File
@@ -497,3 +497,25 @@ fn the_caret_after_a_line_break_sits_on_the_next_line() {
let l = layout(&TextData::new("ab\n"), None, None);
assert_eq!(l.caret(3), (0.0, 1));
}
#[test]
fn lower_case_writes_final_sigma() {
if !have_fonts() {
return;
}
let gids = |t: &TextData| layout(t, None, None).glyphs.iter().map(|g| (g.gid, g.char_start)).collect::<Vec<_>>();
let want = gids(&TextData::new("οδος ας"));
let Some(face) = layout(&TextData::new("ς"), None, None).glyphs.first().map(|g| g.face.clone()) else { return };
if face.glyph_for('σ') == face.glyph_for('ς') {
eprintln!("face has no distinct final sigma; skipping");
return;
}
let mut t = TextData::new("ΟΔΟΣ ΑΣ");
t.style.case = figcraft_doc::TextCase::Lower;
assert_eq!(gids(&t), want, "one run");
// The word split across two styled runs: the final form is still chosen from the whole text.
let mut other = t.style.clone();
other.font_size += 4.0;
t.runs.push(figcraft_doc::StyleRun { start: 3, end: 4, style: other });
assert_eq!(gids(&t).iter().map(|g| g.0).collect::<Vec<_>>(), want.iter().map(|g| g.0).collect::<Vec<_>>(), "split runs");
}