diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 26ae3d33..f844b571 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -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, 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() { diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index ebf5889c..4475fbbc 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -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::>(); + 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::>(), want.iter().map(|g| g.0).collect::>(), "split runs"); +}