Strengthen unwired field geometry RTL evaluators

Assert RTL-leading full and ellipsized paragraph preconditions and complete independent rectangle sets. Record system-font limitations and add one ignored bounded 4096-scalar lookup diagnostic with exact full-coverage oracles. Format the two experiment inputs only. Format and diff checks passed; no compiler, tests, native execution or production wiring.
This commit is contained in:
2026-09-13 03:14:45 -07:00
parent c3ad6f130b
commit 11c605f917
3 changed files with 598 additions and 94 deletions
@@ -160,19 +160,28 @@ impl Label {
return Err(Error::InvalidFieldRange);
}
let mut result = FieldLayout {
rectangles: vec![], covered: vec![], truncated: vec![],
unresolved: vec![], partially_retained: false, shared_cluster: false,
rectangles: vec![],
covered: vec![],
truncated: vec![],
unresolved: vec![],
partially_retained: false,
shared_cluster: false,
};
let mut selected = vec![false; self.clusters.len()];
for span in &self.source_spans {
if span.whitespace || !overlaps(&span.source, &field) { continue; }
if span.whitespace || !overlaps(&span.source, &field) {
continue;
}
// Source spans for text are exact UTF-8 scalars, not intervals
// between ends (which could assign trimmed whitespace to text).
if span.normalized.end > self.retained_normalized {
append_range(&mut result.truncated, span.source.clone());
continue;
}
let matching: Vec<_> = self.clusters.iter().enumerate()
let matching: Vec<_> = self
.clusters
.iter()
.enumerate()
.filter(|(_, cluster)| overlaps(&cluster.normalized, &span.normalized))
.collect();
result.shared_cluster |= matching.iter().any(|(_, cluster)| {
@@ -186,11 +195,20 @@ impl Label {
Some(FieldIssue::NonPaintingScalar)
} else if matching.iter().any(|(_, cluster)| cluster.missing) {
Some(FieldIssue::MissingGlyph)
} else if matching.is_empty() || matching.iter().all(|(_, cluster)| cluster.rectangles.is_empty()) {
} else if matching.is_empty()
|| matching
.iter()
.all(|(_, cluster)| cluster.rectangles.is_empty())
{
Some(FieldIssue::NoAdvanceGeometry)
} else { None };
} else {
None
};
if let Some(reason) = issue {
result.unresolved.push(UnresolvedField { source: span.source.clone(), reason });
result.unresolved.push(UnresolvedField {
source: span.source.clone(),
reason,
});
continue;
}
append_range(&mut result.covered, span.source.clone());
@@ -199,12 +217,17 @@ impl Label {
}
}
for (cluster, selected) in self.clusters.iter().zip(selected) {
if !selected { continue; }
if !selected {
continue;
}
for bounds in &cluster.rectangles {
let translated = translated_bounds(
Point::new(position.x + bounds.x, position.y + bounds.y), bounds.size(),
Point::new(position.x + bounds.x, position.y + bounds.y),
bounds.size(),
)?;
if !result.rectangles.contains(&translated) { result.rectangles.push(translated); }
if !result.rectangles.contains(&translated) {
result.rectangles.push(translated);
}
}
}
result.partially_retained = !result.truncated.is_empty()
@@ -465,10 +488,18 @@ impl Cache {
// measure("") deliberately creates no cache entry.
vec![]
} else {
let (_, entry) = shapes.allocate(fonts.raw(), graphics_text::cache::Key {
content: &displayed, size, line_height, font: style.font,
bounds: Size::INFINITE, shaping: style.shaping, align_x: Alignment::Default,
});
let (_, entry) = shapes.allocate(
fonts.raw(),
graphics_text::cache::Key {
content: &displayed,
size,
line_height,
font: style.font,
bounds: Size::INFINITE,
shaping: style.shaping,
align_x: Alignment::Default,
},
);
field_clusters(entry, zoom)?
};
let label = Arc::new(Label {
@@ -544,7 +575,8 @@ impl Normalized {
result.spans.push(SourceSpan {
source: result.ends.last().expect("initial boundary").1..start,
normalized: normalized_start..result.text.len(),
whitespace: true, non_painting: false,
whitespace: true,
non_painting: false,
});
result.ends.push((result.text.len(), start));
separator = false;
@@ -554,7 +586,8 @@ impl Normalized {
result.spans.push(SourceSpan {
source: start..start + c.len_utf8(),
normalized: normalized_start..result.text.len(),
whitespace: false, non_painting: non_painting_scalar(c),
whitespace: false,
non_painting: non_painting_scalar(c),
});
result.ends.push((result.text.len(), start + c.len_utf8()));
}
@@ -574,7 +607,8 @@ fn overlaps(a: &Range<usize>, b: &Range<usize>) -> bool {
fn append_range(ranges: &mut Vec<Range<usize>>, next: Range<usize>) {
if let Some(last) = ranges.last_mut()
&& last.end == next.start {
&& last.end == next.start
{
last.end = next.end;
return;
}
@@ -586,36 +620,57 @@ fn append_range(ranges: &mut Vec<Range<usize>>, next: Range<usize>) {
// documented in pinned harfrust 0.3.2 src/hb/unicode.rs:615–650. Include its
// spacing-filler exceptions conservatively; this is not an ink classifier.
fn non_painting_scalar(c: char) -> bool {
c.is_control() || matches!(c as u32,
c.is_control()
|| matches!(c as u32,
0x00ad | 0x034f | 0x061c | 0x115f..=0x1160 | 0x17b4..=0x17b5 |
0x180b..=0x180f | 0x200b..=0x200f | 0x202a..=0x202e | 0x2060..=0x206f |
0x3164 | 0xfe00..=0xfe0f | 0xfeff | 0xffa0 | 0xfff0..=0xfff8 |
0x1bca0..=0x1bca3 | 0x1d173..=0x1d17a | 0xe0000..=0xe0fff)
}
fn field_clusters(entry: &graphics_text::cache::Entry, zoom: f32) -> Result<Vec<FieldCluster>, Error> {
fn field_clusters(
entry: &graphics_text::cache::Entry,
zoom: f32,
) -> Result<Vec<FieldCluster>, Error> {
let mut clusters: std::collections::BTreeMap<(usize, usize), FieldCluster> = Default::default();
for run in entry.buffer.layout_runs() {
for glyph in run.glyphs {
if glyph.start >= glyph.end || run.text.get(glyph.start..glyph.end).is_none() {
return Err(Error::InvalidMeasurement);
}
let cluster = clusters.entry((glyph.start, glyph.end)).or_insert_with(|| FieldCluster {
normalized: glyph.start..glyph.end, rectangles: vec![], missing: false,
});
let cluster =
clusters
.entry((glyph.start, glyph.end))
.or_insert_with(|| FieldCluster {
normalized: glyph.start..glyph.end,
rectangles: vec![],
missing: false,
});
cluster.missing |= glyph.glyph_id == 0;
let x1 = glyph.x / zoom;
let x2 = (glyph.x + glyph.w) / zoom;
let y = (run.line_top + glyph.y) / zoom;
let height = glyph.line_height_opt.unwrap_or(run.line_height) / zoom;
if !x1.is_finite() || !x2.is_finite() || !y.is_finite()
|| !height.is_finite() || height < 0. || !(y + height).is_finite() {
if !x1.is_finite()
|| !x2.is_finite()
|| !y.is_finite()
|| !height.is_finite()
|| height < 0.
|| !(y + height).is_finite()
{
return Err(Error::InvalidMeasurement);
}
if x1 != x2 && height > 0. {
let bounds = Rectangle::new(Point::new(x1.min(x2), y), Size::new((x2 - x1).abs(), height));
if !bounds.width.is_finite() { return Err(Error::InvalidMeasurement); }
if !cluster.rectangles.contains(&bounds) { cluster.rectangles.push(bounds); }
let bounds = Rectangle::new(
Point::new(x1.min(x2), y),
Size::new((x2 - x1).abs(), height),
);
if !bounds.width.is_finite() {
return Err(Error::InvalidMeasurement);
}
if !cluster.rectangles.contains(&bounds) {
cluster.rectangles.push(bounds);
}
}
}
}
@@ -8,29 +8,116 @@ use iced::advanced::{
fn paragraph(text: &str, zoom: f32, style: Style) -> Paragraph {
Paragraph::with_text(ParagraphText {
content: text, bounds: Size::INFINITE, size: Pixels(style.size.0 * zoom),
content: text,
bounds: Size::INFINITE,
size: Pixels(style.size.0 * zoom),
line_height: match style.line_height {
LineHeight::Relative(value) => LineHeight::Relative(value),
LineHeight::Absolute(value) => LineHeight::Absolute(Pixels(value.0 * zoom)),
},
font: style.font, align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top, shaping: style.shaping,
font: style.font,
align_x: Alignment::Default,
align_y: iced::alignment::Vertical::Top,
shaping: style.shaping,
wrapping: Wrapping::default(),
})
}
fn resolve(text: &str, width: f32, zoom: f32, style: Style) -> (RenderSession, Arc<Label>) {
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let label = Cache::default().resolve(&session, text, width, zoom, style).unwrap();
let label = Cache::default()
.resolve(&session, text, width, zoom, style)
.unwrap();
(session, label)
}
// The oracle walks an independently built Paragraph, never Label's retained
// spans, cluster snapshot, or field_layout. Test callers supply explicit ranges
// in the already displayed string; synthetic ellipsis is deliberately excluded.
fn paragraph_boxes(oracle: &Paragraph, ranges: &[Range<usize>], zoom: f32) -> Vec<Rectangle> {
let mut boxes = vec![];
for run in oracle.buffer().layout_runs() {
for glyph in run.glyphs {
if !ranges
.iter()
.any(|range| range.start < glyph.end && glyph.start < range.end)
{
continue;
}
assert_ne!(glyph.glyph_id, 0, "oracle requires actual font coverage");
if glyph.w == 0. {
continue;
}
assert!(
glyph.w > 0.,
"these fixtures require positive glyph advances"
);
let left = glyph.x / zoom;
let right = (glyph.x + glyph.w) / zoom;
boxes.push(Rectangle::new(
Point::new(left, (run.line_top + glyph.y) / zoom),
Size::new(
right - left,
glyph.line_height_opt.unwrap_or(run.line_height) / zoom,
),
));
}
}
canonical_boxes(boxes)
}
fn canonical_boxes(mut boxes: Vec<Rectangle>) -> Vec<Rectangle> {
boxes.sort_by(|a, b| {
a.x.total_cmp(&b.x)
.then(a.y.total_cmp(&b.y))
.then(a.width.total_cmp(&b.width))
.then(a.height.total_cmp(&b.height))
});
boxes.dedup();
boxes
}
fn assert_complete_boxes(actual: &[Rectangle], expected: Vec<Rectangle>) {
let canonical = canonical_boxes(actual.to_vec());
assert_eq!(
actual.len(),
canonical.len(),
"duplicate returned boxes are extraneous"
);
assert!(
!expected.is_empty(),
"the geometric oracle must be nonempty"
);
assert_eq!(
canonical, expected,
"every selected rectangle and only selected rectangles"
);
}
fn resolved_fonts(oracle: &Paragraph) -> Vec<String> {
let mut identities = vec![];
let mut fonts = graphics_text::font_system().write().unwrap();
for glyph in oracle.buffer().layout_runs().flat_map(|run| run.glyphs) {
let face = fonts.raw().db().face(glyph.font_id).unwrap();
let identity = format!(
"{:?}: {} {:?}",
glyph.font_id, face.post_script_name, face.families
);
if !identities.contains(&identity) {
identities.push(identity);
}
}
identities
}
#[test]
fn empty_and_ellipsis_only_have_no_field_coverage() {
for text in ["", " \t\n", "Operation: assess"] {
let (session, label) = resolve(text, 0., 1., Style::default());
assert!(label.displayed.is_empty());
let field = label.field_layout(&session, 1., Point::ORIGIN, 0..text.len()).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 0..text.len())
.unwrap();
assert!(field.rectangles.is_empty());
assert!(field.covered.is_empty());
assert!(field.unresolved.is_empty());
@@ -39,8 +126,13 @@ fn empty_and_ellipsis_only_have_no_field_coverage() {
let width = paragraph("…", 1., style).min_width();
let text = "Operation: assess";
let (session, label) = resolve(text, width, 1., style);
assert_eq!(label.displayed, "…", "must exercise synthetic-only displayed content");
let field = label.field_layout(&session, 1., Point::ORIGIN, 0..text.len()).unwrap();
assert_eq!(
label.displayed, "…",
"must exercise synthetic-only displayed content"
);
let field = label
.field_layout(&session, 1., Point::ORIGIN, 0..text.len())
.unwrap();
assert!(field.covered.is_empty());
assert!(field.rectangles.is_empty());
assert!(!field.truncated.is_empty());
@@ -52,12 +144,16 @@ fn exact_scalar_mapping_does_not_assign_trimmed_or_collapsed_whitespace_to_text(
let (session, label) = resolve(source, 900., 1., Style::default());
assert_eq!(label.displayed, "send assess");
for range in [0..2, 6..10, 16..18] {
let result = label.field_layout(&session, 1., Point::ORIGIN, range).unwrap();
let result = label
.field_layout(&session, 1., Point::ORIGIN, range)
.unwrap();
assert!(result.covered.is_empty());
assert!(result.rectangles.is_empty());
assert!(result.unresolved.is_empty());
}
let result = label.field_layout(&session, 1., Point::ORIGIN, 10..16).unwrap();
let result = label
.field_layout(&session, 1., Point::ORIGIN, 10..16)
.unwrap();
assert_eq!(result.covered, vec![10..16]);
assert!(!result.rectangles.is_empty());
assert!(result.truncated.is_empty());
@@ -68,24 +164,58 @@ fn exact_scalar_mapping_does_not_assign_trimmed_or_collapsed_whitespace_to_text(
fn late_operation_reports_hidden_partial_and_complete_without_ellipsis_membership() {
let source = "1. call - Incident report - assess_pressure";
let start = source.find("assess_pressure").unwrap();
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let (session, full) = resolve(source, 900., 1., style);
let complete = full.field_layout(&session, 1., Point::ORIGIN, start..source.len()).unwrap();
let complete = full
.field_layout(&session, 1., Point::ORIGIN, start..source.len())
.unwrap();
assert_eq!(complete.covered, vec![start..source.len()]);
assert!(!complete.partially_retained);
let (_, hidden) = resolve(source, paragraph("1. call…", 1., style).min_width(), 1., style);
let hidden_result = hidden.field_layout(&hidden.measured_session, 1., Point::ORIGIN, start..source.len()).unwrap();
let (_, hidden) = resolve(
source,
paragraph("1. call…", 1., style).min_width(),
1.,
style,
);
let hidden_result = hidden
.field_layout(
&hidden.measured_session,
1.,
Point::ORIGIN,
start..source.len(),
)
.unwrap();
assert!(hidden.retained_source.end <= start);
assert!(hidden_result.covered.is_empty());
assert_eq!(hidden_result.truncated, vec![start..source.len()]);
assert!(!hidden_result.partially_retained);
let desired = format!("{}assess…", &source[..start]);
let (_, partial) = resolve(source, paragraph(&desired, 1., style).min_width(), 1., style);
assert!(partial.retained_source.end > start && partial.retained_source.end < source.len(),
"actual fitting result must retain a strict operation prefix");
let result = partial.field_layout(&partial.measured_session, 1., Point::ORIGIN, start..source.len()).unwrap();
let (_, partial) = resolve(
source,
paragraph(&desired, 1., style).min_width(),
1.,
style,
);
assert!(
partial.retained_source.end > start && partial.retained_source.end < source.len(),
"actual fitting result must retain a strict operation prefix"
);
let result = partial
.field_layout(
&partial.measured_session,
1.,
Point::ORIGIN,
start..source.len(),
)
.unwrap();
assert_eq!(result.covered, vec![start..partial.retained_source.end]);
assert_eq!(result.truncated, vec![partial.retained_source.end..source.len()]);
assert_eq!(
result.truncated,
vec![partial.retained_source.end..source.len()]
);
assert!(result.partially_retained);
assert!(result.unresolved.is_empty());
}
@@ -93,18 +223,36 @@ fn late_operation_reports_hidden_partial_and_complete_without_ellipsis_membershi
#[test]
fn positive_notdef_advance_is_unresolved_not_covered_or_truncated() {
let source = "A\u{10ffff}B";
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
assert!(oracle.buffer().layout_runs().flat_map(|run| run.glyphs)
.any(|glyph| glyph.start == 1 && glyph.end == 5 && glyph.glyph_id == 0 && glyph.w > 0.),
"font precondition: actual positive-width .notdef for the noncharacter");
assert!(
oracle
.buffer()
.layout_runs()
.flat_map(|run| run.glyphs)
.any(|glyph| glyph.start == 1 && glyph.end == 5 && glyph.glyph_id == 0 && glyph.w > 0.),
"font precondition: actual positive-width .notdef for the noncharacter"
);
let (session, label) = resolve(source, 900., 1., style);
let missing = label.field_layout(&session, 1., Point::ORIGIN, 1..5).unwrap();
let missing = label
.field_layout(&session, 1., Point::ORIGIN, 1..5)
.unwrap();
assert!(missing.covered.is_empty());
assert!(missing.rectangles.is_empty());
assert!(missing.truncated.is_empty());
assert_eq!(missing.unresolved, vec![UnresolvedField { source: 1..5, reason: FieldIssue::MissingGlyph }]);
let mixed = label.field_layout(&session, 1., Point::ORIGIN, 0..source.len()).unwrap();
assert_eq!(
missing.unresolved,
vec![UnresolvedField {
source: 1..5,
reason: FieldIssue::MissingGlyph
}]
);
let mixed = label
.field_layout(&session, 1., Point::ORIGIN, 0..source.len())
.unwrap();
assert_eq!(mixed.covered, vec![0..1, 5..6]);
assert_eq!(mixed.unresolved, missing.unresolved);
assert!(!mixed.rectangles.is_empty());
@@ -113,93 +261,348 @@ fn positive_notdef_advance_is_unresolved_not_covered_or_truncated() {
#[test]
fn advanced_ligature_partial_field_uses_whole_shared_cluster_not_fraction() {
let source = "ffi";
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
let glyph = run.glyphs.iter().find(|glyph| glyph.start == 0 && glyph.end == 3 && glyph.w > 0. && glyph.glyph_id != 0)
let glyph = run
.glyphs
.iter()
.find(|glyph| glyph.start == 0 && glyph.end == 3 && glyph.w > 0. && glyph.glyph_id != 0)
.expect("font precondition: Advanced must produce the ffi ligature cluster");
let expected = Rectangle::new(Point::new(glyph.x, run.line_top + glyph.y),
Size::new(glyph.w, glyph.line_height_opt.unwrap_or(run.line_height)));
let expected = Rectangle::new(
Point::new(glyph.x, run.line_top + glyph.y),
Size::new(glyph.w, glyph.line_height_opt.unwrap_or(run.line_height)),
);
let (session, label) = resolve(source, 900., 1., style);
let field = label.field_layout(&session, 1., Point::ORIGIN, 1..2).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 1..2)
.unwrap();
assert_eq!(field.covered, vec![1..2]);
assert!(field.shared_cluster);
assert!(field.rectangles.contains(&expected));
assert_eq!(field.rectangles.iter().map(|rect| rect.width).sum::<f32>(), expected.width);
assert_eq!(
field.rectangles.iter().map(|rect| rect.width).sum::<f32>(),
expected.width
);
assert!(field.unresolved.is_empty());
}
#[test]
fn shared_default_ignorable_does_not_inherit_neighbor_coverage() {
let source = "a\u{fe0f}";
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
assert!(oracle.buffer().layout_runs().flat_map(|run| run.glyphs)
.any(|glyph| glyph.start == 0 && glyph.end == source.len() && glyph.w > 0. && glyph.glyph_id != 0),
"precondition: variation selector shares a real positive-width base cluster");
assert!(
oracle
.buffer()
.layout_runs()
.flat_map(|run| run.glyphs)
.any(|glyph| glyph.start == 0
&& glyph.end == source.len()
&& glyph.w > 0.
&& glyph.glyph_id != 0),
"precondition: variation selector shares a real positive-width base cluster"
);
let (session, label) = resolve(source, 900., 1., style);
let field = label.field_layout(&session, 1., Point::ORIGIN, 1..source.len()).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 1..source.len())
.unwrap();
assert!(field.covered.is_empty());
assert!(field.rectangles.is_empty());
assert!(field.truncated.is_empty());
assert!(field.shared_cluster);
assert_eq!(field.unresolved, vec![UnresolvedField { source: 1..source.len(), reason: FieldIssue::NonPaintingScalar }]);
assert_eq!(
field.unresolved,
vec![UnresolvedField {
source: 1..source.len(),
reason: FieldIssue::NonPaintingScalar
}]
);
}
#[test]
fn combining_mark_field_retains_cluster_geometry_without_splitting_the_grapheme() {
let source = "e\u{301} next";
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
let cluster = run.glyphs.iter().find(|glyph| glyph.start == 0 && glyph.end == 3 && glyph.w > 0. && glyph.glyph_id != 0)
let cluster = run
.glyphs
.iter()
.find(|glyph| glyph.start == 0 && glyph.end == 3 && glyph.w > 0. && glyph.glyph_id != 0)
.expect("precondition: composed accent and base share the grapheme cluster");
let (session, label) = resolve(source, 900., 1., style);
let field = label.field_layout(&session, 1., Point::ORIGIN, 1..3).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 1..3)
.unwrap();
assert_eq!(field.covered, vec![1..3]);
assert!(field.shared_cluster);
assert!(field.rectangles.iter().any(|rect| rect.x == cluster.x && rect.width == cluster.w));
assert!(
field
.rectangles
.iter()
.any(|rect| rect.x == cluster.x && rect.width == cluster.w)
);
assert!(field.unresolved.is_empty());
assert_eq!(label.field_layout(&session, 1., Point::ORIGIN, 2..3), Err(Error::InvalidFieldRange));
let (_, short) = resolve(source, paragraph("e\u{301}…", 1., style).min_width(), 1., style);
assert_ne!(short.retained_source.end, 1, "the chosen prefix must never split the combining grapheme");
assert_eq!(
label.field_layout(&session, 1., Point::ORIGIN, 2..3),
Err(Error::InvalidFieldRange)
);
let (_, short) = resolve(
source,
paragraph("e\u{301}…", 1., style).min_width(),
1.,
style,
);
assert_ne!(
short.retained_source.end, 1,
"the chosen prefix must never split the combining grapheme"
);
}
#[test]
fn bidi_logical_field_keeps_visual_pieces_without_enclosing_unselected_glyph() {
let source = "abc אבג xyz";
let style = Style { shaping: Shaping::Advanced, ..Style::default() };
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
assert!(
!run.rtl,
"this existing fixture specifically has an LTR paragraph base"
);
assert!(run.glyphs.iter().any(|glyph| glyph.level.is_rtl()));
assert!(run.glyphs.iter().any(|glyph| !glyph.level.is_rtl()));
let excluded = run.glyphs.iter().find(|glyph| glyph.start == 8 && glyph.end == 10 && glyph.w > 0. && glyph.glyph_id != 0).unwrap();
let excluded = run
.glyphs
.iter()
.find(|glyph| glyph.start == 8 && glyph.end == 10 && glyph.w > 0. && glyph.glyph_id != 0)
.unwrap();
let (session, label) = resolve(source, 900., 1., style);
let field = label.field_layout(&session, 1., Point::ORIGIN, 1..8).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 1..8)
.unwrap();
assert_eq!(field.covered, vec![1..3, 4..8]);
assert!(field.unresolved.is_empty());
assert!(field.rectangles.iter().any(|rect| rect.x + rect.width <= excluded.x));
assert!(field.rectangles.iter().any(|rect| rect.x >= excluded.x + excluded.w));
assert!(field.rectangles.iter().all(|rect|
rect.x + rect.width <= excluded.x || rect.x >= excluded.x + excluded.w),
"logical selection must not become one envelope over the intervening visual glyph");
assert_complete_boxes(
&field.rectangles,
paragraph_boxes(&oracle, &[1..3, 4..8], 1.),
);
assert!(
field
.rectangles
.iter()
.any(|rect| rect.x + rect.width <= excluded.x)
);
assert!(
field
.rectangles
.iter()
.any(|rect| rect.x >= excluded.x + excluded.w)
);
assert!(
field
.rectangles
.iter()
.all(|rect| rect.x + rect.width <= excluded.x || rect.x >= excluded.x + excluded.w),
"logical selection must not become one envelope over the intervening visual glyph"
);
}
#[test]
fn rtl_leading_full_field_has_complete_post_alignment_rectangle_set() {
let source = "אבג abc דהו xyz operation";
assert_eq!(source.len(), 31);
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let oracle = paragraph(source, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
assert!(
run.rtl,
"must exercise Iced's RTL paragraph alignment branch"
);
assert!(run.glyphs.iter().any(|glyph| glyph.level.is_rtl()));
assert!(run.glyphs.iter().any(|glyph| !glyph.level.is_rtl()));
eprintln!("RTL_FIELD_FONTS={:?}", resolved_fonts(&oracle));
let (session, label) = resolve(source, 900., 1., style);
assert_eq!(label.displayed, source);
let ranges = vec![2..6, 7..10, 11..17, 18..21, 22..31];
let field = label
.field_layout(&session, 1., Point::ORIGIN, 2..source.len())
.unwrap();
assert_eq!(field.covered, ranges);
assert!(field.truncated.is_empty() && field.unresolved.is_empty());
assert!(!field.partially_retained);
assert_complete_boxes(&field.rectangles, paragraph_boxes(&oracle, &ranges, 1.));
}
#[test]
fn rtl_leading_ellipsis_field_has_complete_final_buffer_rectangle_set() {
let source = "אבג abc דהו xyz operation";
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let width = paragraph("אבג abc דהו…", 1., style).min_width();
let (session, label) = resolve(source, width, 1., style);
let prefix = label
.displayed
.strip_suffix('…')
.expect("must use an ellipsized displayed buffer");
assert!(prefix.len() >= 10 && prefix.len() < source.len());
assert!(source.starts_with(prefix));
let oracle = paragraph(&label.displayed, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
assert!(
run.rtl,
"the ellipsized winning text must still exercise RTL alignment"
);
eprintln!("RTL_ELLIPSIS_FIELD_FONTS={:?}", resolved_fonts(&oracle));
// Fixed semantic word spans from the source, clipped only by the literal
// displayed prefix. No candidate source map or retained_source is consulted.
let words = [2..6, 7..10, 11..17, 18..21, 22..31];
let retained: Vec<_> = words
.iter()
.filter_map(|word| {
let end = word.end.min(prefix.len());
(word.start < end).then_some(word.start..end)
})
.collect();
let hidden: Vec<_> = words
.iter()
.filter_map(|word| {
let start = word.start.max(prefix.len());
(start < word.end).then_some(start..word.end)
})
.collect();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 2..source.len())
.unwrap();
assert_eq!(field.covered, retained);
assert_eq!(field.truncated, hidden);
assert!(field.partially_retained && field.unresolved.is_empty());
assert_complete_boxes(&field.rectangles, paragraph_boxes(&oracle, &retained, 1.));
}
/// Diagnostic only: warm font/Paragraph preparation precedes the timed resolver
/// and three lookups. No latency budget or production responsiveness claim.
#[test]
#[ignore = "bounded high-end field lookup diagnostic; run this exact test alone with --nocapture"]
fn field_lookup_4096_scalars_and_combining_heavy_full_coverage() {
let style = Style {
shaping: Shaping::Advanced,
..Style::default()
};
let cases = [
("4096_plain_scalars", "A".repeat(4096), 4096),
(
"4096_combining_heavy_scalars",
"a\u{301}\u{323}\u{327}".repeat(1024),
1024,
),
];
let mut records = vec![];
for (name, source, expected_graphemes) in cases {
assert_eq!(source.chars().count(), 4096);
assert_eq!(source.graphemes(true).count(), expected_graphemes);
assert!(source.len() <= MAX_SOURCE_BYTES);
assert!(expected_graphemes <= MAX_GRAPHEMES);
let oracle = paragraph(&source, 1., style);
assert_eq!(oracle.buffer().layout_runs().count(), 1);
let expected = paragraph_boxes(&oracle, &[0..source.len()], 1.);
let identities = resolved_fonts(&oracle);
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
let mut cache = Cache::default();
let start = std::time::Instant::now();
let label = cache
.resolve(&session, &source, 1_000_000., 1., style)
.unwrap();
let resolve_ns = start.elapsed().as_nanos();
assert_eq!(label.displayed, source);
let mut lookup_ns = vec![];
for _ in 0..3 {
let start = std::time::Instant::now();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 0..source.len())
.unwrap();
lookup_ns.push(start.elapsed().as_nanos());
assert_eq!(field.covered, vec![0..source.len()]);
assert!(field.truncated.is_empty() && field.unresolved.is_empty());
assert!(!field.partially_retained && !field.shared_cluster);
assert_complete_boxes(&field.rectangles, expected.clone());
}
records.push(serde_json::json!({
"case": name, "scalars": 4096, "graphemes": expected_graphemes,
"source_bytes": source.len(), "expected_rectangles": expected.len(),
"resolved_fonts": identities, "resolve_ns": resolve_ns, "lookup_ns": lookup_ns,
"full_coverage_and_exact_geometry": "passed",
}));
}
println!(
"FIELD_LOOKUP_TIMING_JSON={}",
serde_json::json!({
"schema": "scenario_label_field_lookup_v1",
"source_pin_from_runner": std::env::var("SCENARIO_LABEL_FIELDS_SOURCE_PIN").ok(),
"debug_assertions": cfg!(debug_assertions), "os": std::env::consts::OS,
"arch": std::env::consts::ARCH, "cases": records,
"invariant_status": "passed",
"scope": "warm-font source-only layout experiment; no native responsiveness threshold",
})
);
}
#[test]
fn auto_ascii_to_ellipsis_advanced_transition_uses_final_buffer() {
let style = Style { shaping: Shaping::Auto, ..Style::default() };
let style = Style {
shaping: Shaping::Auto,
..Style::default()
};
let source = "ffi office assessment";
assert_eq!(graphics_text::to_shaping(style.shaping, source), graphics_text::cosmic_text::Shaping::Basic);
let (session, label) = resolve(source, paragraph("ffi office…", 1., style).min_width(), 1., style);
assert_eq!(
graphics_text::to_shaping(style.shaping, source),
graphics_text::cosmic_text::Shaping::Basic
);
let (session, label) = resolve(
source,
paragraph("ffi office…", 1., style).min_width(),
1.,
style,
);
assert!(label.displayed.ends_with('…'));
assert_eq!(graphics_text::to_shaping(style.shaping, &label.displayed), graphics_text::cosmic_text::Shaping::Advanced);
assert_eq!(
graphics_text::to_shaping(style.shaping, &label.displayed),
graphics_text::cosmic_text::Shaping::Advanced
);
let oracle = paragraph(&label.displayed, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
let first = run.glyphs.iter().find(|glyph| glyph.start == 0 && glyph.end >= 3 && glyph.w > 0. && glyph.glyph_id != 0)
let first = run
.glyphs
.iter()
.find(|glyph| glyph.start == 0 && glyph.end >= 3 && glyph.w > 0. && glyph.glyph_id != 0)
.expect("final Advanced shape must expose the ligature precondition");
let field = label.field_layout(&session, 1., Point::ORIGIN, 0..3).unwrap();
let field = label
.field_layout(&session, 1., Point::ORIGIN, 0..3)
.unwrap();
assert_eq!(field.covered, vec![0..3]);
assert!(field.rectangles.iter().any(|rect| rect.x == first.x && rect.width == first.w));
assert!(
field
.rectangles
.iter()
.any(|rect| rect.x == first.x && rect.width == first.w)
);
}
#[test]
@@ -215,15 +618,36 @@ fn field_geometry_reuses_zoom_session_guards_and_translates_actual_cluster_boxes
let (session, label) = resolve(source, 900., zoom, style);
let position = Point::new(-300., 90.);
let field = label.field_layout(&session, zoom, position, 0..1).unwrap();
assert_eq!(field.rectangles, vec![Rectangle::new(
Point::new(position.x + glyph.x / zoom, position.y + (run.line_top + glyph.y) / zoom),
Size::new(((glyph.x + glyph.w) / zoom - glyph.x / zoom).abs(), glyph.line_height_opt.unwrap_or(run.line_height) / zoom),
)]);
assert_eq!(label.field_layout(&session, zoom + 0.1, position, 0..1), Err(Error::StaleZoom));
assert_eq!(
field.rectangles,
vec![Rectangle::new(
Point::new(
position.x + glyph.x / zoom,
position.y + (run.line_top + glyph.y) / zoom
),
Size::new(
((glyph.x + glyph.w) / zoom - glyph.x / zoom).abs(),
glyph.line_height_opt.unwrap_or(run.line_height) / zoom
),
)]
);
assert_eq!(
label.field_layout(&session, zoom + 0.1, position, 0..1),
Err(Error::StaleZoom)
);
let fresh = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
assert_eq!(label.field_layout(&fresh, zoom, position, 0..1), Err(Error::StaleRenderSession));
assert_eq!(label.field_layout(&session, zoom, Point::new(f32::NAN, 0.), 0..1), Err(Error::InvalidPosition));
assert_eq!(label.field_layout(&session, zoom, position, 0..2), Err(Error::InvalidFieldRange));
assert_eq!(
label.field_layout(&fresh, zoom, position, 0..1),
Err(Error::StaleRenderSession)
);
assert_eq!(
label.field_layout(&session, zoom, Point::new(f32::NAN, 0.), 0..1),
Err(Error::InvalidPosition)
);
assert_eq!(
label.field_layout(&session, zoom, position, 0..2),
Err(Error::InvalidFieldRange)
);
let empty = label.field_layout(&session, zoom, position, 0..0).unwrap();
assert!(empty.covered.is_empty() && empty.rectangles.is_empty());
}
@@ -0,0 +1,25 @@
# Unwired Scenario label field experiment
This experiment starts from `c4a2cf63f11818cf7fc766acf3a2779768a2a658`. Initial extraction is commit `c3ad6f130ba6dda271fe8417db46df29e4033ad8`; the follow-up strengthens its evaluators. Neither version wires `scenario_label` into production. No renderer, App, navigation, source language, or model behavior is adopted here.
The implementation lives in `crates/oa-desktop/src/behavior/scenario_label.rs`, with focused evaluators in `scenario_label/field_tests.rs`. It preserves the winning shaped buffer's cluster advance rectangles and exact scalar source mapping. Returned covered, truncated, and unresolved ranges are distinct; partial retention and shared clusters are explicit. Synthetic ellipsis never represents source text. Missing glyphs, controls/default-ignorables, and absent advance geometry cannot establish affirmative coverage. These are advance/hit rectangles, not ink bounds or proof of actual viewport visibility.
## Follow-up evaluator corrections
The original mixed bidi fixture is Latin-leading. Its glyph embedding levels are mixed, but `LayoutRun::rtl` is false, so it cannot exercise Iced's RTL paragraph alignment branch. Two added cases require a genuinely RTL-leading full and ellipsized paragraph and assert `run.rtl == true`. Their geometry oracle builds a separate Iced Paragraph using the actual displayed string and compares the **complete rectangle set**, rejecting omissions, extras and duplicate output boxes. Explicit source word spans are clipped only against the literal displayed prefix before ellipsis; no candidate retained-source map or cluster snapshot supplies the expected answer. The existing mixed-direction case now uses the same complete-set comparison in addition to its visual-gap checks.
The relevant pinned sources are `iced_graphics 0.14.0/src/text.rs:202–256` (measurement and RTL alignment), `src/text/cache.rs` (winning buffer), and `cosmic-text 0.15.0/src/buffer.rs`/`layout.rs` (actual layout runs and clusters). `iced_widget` is 0.14.2; the graphics package is 0.14.0. Reproduce package inputs through the base commit's Cargo.lock.
## Font and execution limitations
These fixtures use the installed system default font selection; they do **not** pin font bytes. The strong ligature, supported-glyph, shared-cluster and bidi preconditions must fail visibly if the local font environment cannot exercise the intended case. RTL cases print resolved font IDs, PostScript names and family records from the actual Paragraph. Font IDs are process-local and these names are diagnostic metadata, not a portable content hash. No global font mutation or new dependency is introduced. A missing precondition is an evaluator/environment finding, not proof that field geometry passed.
There are twelve normal field tests and one ignored diagnostic in the new module; all remain **UNEXECUTED** at this handoff. Existing label evaluators are retained. Because the parent module remains unwired, ordinary desktop listing does not include these tests until an explicitly isolated evaluator harness wires it. Formatting and `git diff --check` are source checks only, not compilation or execution evidence.
## Optional bounded lookup diagnostic
Ignored leaf name: `field_lookup_4096_scalars_and_combining_heavy_full_coverage`. Run only this exact test, once an isolated compiled harness exists, with `--ignored --exact <actual-listed-name> --nocapture --test-threads=1`; obtain the qualified name from its real listing. Set `SCENARIO_LABEL_FIELDS_SOURCE_PIN` to the tested commit and separately bind the executable, source and toolchain in the runner receipt.
It measures one resolver call and three full-field lookups for each of two cases: 4,096 ordinary scalars/graphemes, and 4,096 scalars arranged as 1,024 combining-heavy graphemes. Both remain within the existing 64-KiB source/4,096-grapheme experiment budgets. An independent Paragraph warms the font machinery before timing, checks glyph coverage and supplies the complete expected rectangle set. Every lookup must cover the full original source, retain no unresolved/truncated ranges, and match that set exactly; its invariants are checked outside the timed interval.
On success it prints `FIELD_LOOKUP_TIMING_JSON=` with schema `scenario_label_field_lookup_v1`, per-case counts/font identities/resolver nanoseconds/three lookup samples, profile/OS/architecture, optional runner pin, and an explicit passed invariant status. It has no latency threshold and makes no native responsiveness claim. No optimization has been attempted before measurement. The combining case stresses clustered source mapping, while the plain case reaches the existing grapheme budget; neither claims exhaustive worst-case performance over every permitted 64-KiB input.