261 lines
9.8 KiB
Rust
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:#?}"
|
|
);
|
|
}
|