Files
ARCADIA-Studio/crates/oa-desktop/src/source_annotations/anchor_tests.rs
T

261 lines
9.8 KiB
Rust

//! 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:#?}"
);
}