//! Manual actual-font witnesses; missing supported glyphs fail fixture admission. use super::*; use iced::advanced::{ graphics::text::Editor, text::{ Editor as _, Highlighter as _, LineHeight, Wrapping, highlighter::{Format, PlainText}, }, }; use iced::{Font, Pixels}; fn shaped(source: &str, width: f32) -> Editor { let mut editor = Editor::with_text(source); let mut highlighter = PlainText::new(&()); editor.update( Size::new(width, 1200.), Font::MONOSPACE, Pixels(16.), LineHeight::Absolute(Pixels(24.)), Wrapping::WordOrGlyph, &mut highlighter, ); editor.highlight(Font::MONOSPACE, &mut highlighter, |_| Format::default()); editor } fn trailing(cluster: &[&cosmic_text::LayoutGlyph]) -> f32 { let rtl = cluster[0].level.is_rtl(); assert!(cluster.iter().all(|g| g.level.is_rtl() == rtl)); if rtl { cluster .iter() .map(|g| g.x.min(g.x + g.w)) .fold(f32::INFINITY, f32::min) } else { cluster .iter() .map(|g| g.x.max(g.x + g.w)) .fold(f32::NEG_INFINITY, f32::max) } } #[test] #[ignore = "manual supported-font cluster and bidi witnesses"] fn final_multi_glyph_cluster_anchor_uses_logical_trailing_advance_edge() { // Marks without precomposed forms exercise real multi-glyph shaping. Test // both paragraph directions and both final-cluster directions. No font is // assumed to support a given specimen; glyph_id=0 cannot admit a witness. let sources = [ "abc q\u{301}\u{323}", "אבג q\u{301}\u{323}", "abc א\u{5b0}\u{5bc}", "אבג א\u{5b0}\u{5bc}", "abc ب\u{64e}\u{651}", "אבג ب\u{64e}\u{651}", "abc x\u{20dd}", "אבג x\u{20dd}", ]; let mut admitted = 0; let mut opposite_direction = 0; for source in sources { let editor = shaped(source, 700.); let runs: Vec<_> = editor.buffer().layout_runs().collect(); assert_eq!(runs.len(), 1, "wide specimen unexpectedly wrapped"); let run = &runs[0]; let Some(start) = run .glyphs .iter() .filter(|g| g.end == source.len()) .map(|g| g.start) .max() else { continue; }; let cluster: Vec<_> = run .glyphs .iter() .filter(|g| g.start == start && g.end == source.len()) .collect(); if cluster.len() < 2 || cluster.iter().any(|g| g.glyph_id == 0) { continue; } admitted += 1; opposite_direction += usize::from(cluster[0].level.is_rtl() != run.rtl); let expected = trailing(&cluster); let actual = anchor(editor.buffer(), run, source.len()); eprintln!( "EOF witness {source:?}: paragraph_rtl={} cluster={cluster:?} expected={expected} actual={actual:?}", run.rtl ); assert!( actual.is_some_and(|x| (x - expected).abs() < 0.01), "logical EOF must follow the whole cluster, including shared-range glyphs; source={source:?}" ); } assert!( admitted > 0, "fixture admission: current fonts produced no supported multi-glyph final cluster" ); assert!( opposite_direction > 0, "fixture admission: no supported final cluster opposed its paragraph direction" ); } #[test] #[ignore = "manual supported-font cluster and bidi witnesses"] fn mixed_direction_omitted_separator_has_one_logical_predecessor_anchor() { let sources = [ "one אבג דהו two זחט three יכל four מנס five עפצ six קרש seven", "אבג one two דהו three four זחט five six יכל seven eight מנס nine", ]; let mut admitted = 0; let mut rtl_predecessors = 0; let mut failures = Vec::new(); let mut old_mismatches = 0; let mut old_inside_mismatches = 0; for source in sources { for width in [72., 96., 120., 144., 180.] { let editor = shaped(source, width); let runs: Vec<_> = editor.buffer().layout_runs().collect(); assert!(runs.len() > 1); assert_eq!( runs.len(), editor.buffer().lines[0].layout_opt().unwrap().len(), "negative control requires the complete unscrolled paragraph" ); assert!( runs.iter() .any(|r| r.glyphs.iter().any(|g| g.level.is_rtl())) ); assert!( runs.iter() .any(|r| r.glyphs.iter().any(|g| !g.level.is_rtl())) ); for (byte, character) in source.char_indices().filter(|(_, c)| *c == ' ') { // Require a genuinely omitted separator, not a rendered space. if runs .iter() .any(|r| r.glyphs.iter().any(|g| g.start <= byte && byte < g.end)) { continue; } let Some(end) = runs .iter() .flat_map(|r| r.glyphs) .filter(|g| g.end <= byte) .map(|g| g.end) .max() else { continue; }; let Some(next) = runs .iter() .flat_map(|r| r.glyphs) .filter(|g| g.start >= byte + character.len_utf8()) .map(|g| g.start) .min() else { continue; }; if !source[end..next].chars().all(char::is_whitespace) { continue; } let previous: Vec<_> = runs .iter() .enumerate() .filter(|(_, r)| r.glyphs.iter().any(|g| g.end == end)) .collect(); // A cluster broken across runs is a separate boundary problem; // it is not admitted as this omitted-separator witness. if previous.len() != 1 { continue; } let (owner, run) = previous[0]; let start = run .glyphs .iter() .filter(|g| g.end == end) .map(|g| g.start) .max() .unwrap(); let cluster: Vec<_> = run .glyphs .iter() .filter(|g| g.start == start && g.end == end) .collect(); if cluster.iter().any(|g| g.glyph_id == 0) { continue; } admitted += 1; rtl_predecessors += usize::from(cluster[0].level.is_rtl()); let expected = trailing(&cluster); let actual: Vec<_> = runs .iter() .enumerate() .filter_map(|(i, r)| anchor(editor.buffer(), r, byte).map(|x| (i, x))) .collect(); let matches_expected = |positions: &[(usize, f32)]| { positions.len() == 1 && positions[0].0 == owner && (positions[0].1 - expected).abs() < 0.01 }; // Exact rejected fallback, for already-proven omitted bytes: // per-run maximum end and the next visual run's minimum start. let old: Vec<_> = runs .iter() .enumerate() .filter_map(|(i, r)| { let last = r.glyphs.iter().max_by_key(|g| g.end)?; let next_start = runs .get(i + 1) .and_then(|next| next.glyphs.iter().map(|g| g.start).min()); (byte >= last.end && (next_start.is_some_and(|start| byte < start) || (next_start.is_none() && byte == source.len()))) .then_some(( i, if last.level.is_rtl() { last.x } else { last.x + last.w }, )) }) .collect(); let inside_clip = expected >= 0. && expected <= width; if !matches_expected(&old) { old_mismatches += 1; old_inside_mismatches += usize::from(inside_clip); } eprintln!( "gap witness width={width} source={source:?} byte={byte} owner={owner} expected={expected} inside_horizontal_clip={inside_clip} actual={actual:?} rejected_old={old:?} cluster={cluster:?}" ); if !matches_expected(&actual) { failures.push(format!("width={width} source={source:?} byte={byte} expected=({owner},{expected}) inside={inside_clip} actual={actual:?}")); } } } } assert!( admitted > 0, "fixture admission: no omitted mixed-direction separator found" ); assert!( rtl_predecessors > 0, "fixture admission: no omitted separator after a supported RTL cluster" ); eprintln!( "gap summary admitted={admitted} rejected_old_mismatches={old_mismatches} rejected_old_inside_horizontal_clip={old_inside_mismatches}" ); assert!( old_mismatches > 0, "actual-layout negative control must discriminate the old fallback" ); assert!( failures.is_empty(), "mixed-bidi logical anchor failures: {failures:#?}" ); }