From 11c605f9170eae461913410f83f333e9314714a5 Mon Sep 17 00:00:00 2001 From: Codex-ytr83r Date: Sun, 13 Sep 2026 03:14:45 -0700 Subject: [PATCH] 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. --- .../oa-desktop/src/behavior/scenario_label.rs | 109 +++- .../behavior/scenario_label/field_tests.rs | 558 +++++++++++++++--- .../scenario-label-fields-20260913/README.md | 25 + 3 files changed, 598 insertions(+), 94 deletions(-) create mode 100644 harness/experiments/scenario-label-fields-20260913/README.md diff --git a/crates/oa-desktop/src/behavior/scenario_label.rs b/crates/oa-desktop/src/behavior/scenario_label.rs index 0565fdef..bbef18f1 100644 --- a/crates/oa-desktop/src/behavior/scenario_label.rs +++ b/crates/oa-desktop/src/behavior/scenario_label.rs @@ -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, b: &Range) -> bool { fn append_range(ranges: &mut Vec>, next: Range) { 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>, next: Range) { // 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, Error> { +fn field_clusters( + entry: &graphics_text::cache::Entry, + zoom: f32, +) -> Result, 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); + } } } } diff --git a/crates/oa-desktop/src/behavior/scenario_label/field_tests.rs b/crates/oa-desktop/src/behavior/scenario_label/field_tests.rs index bea97253..e83e173c 100644 --- a/crates/oa-desktop/src/behavior/scenario_label/field_tests.rs +++ b/crates/oa-desktop/src/behavior/scenario_label/field_tests.rs @@ -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