Experiment with unwired shaped label field coverage

Preserve winning-buffer cluster geometry and exact scalar mappings. Distinguish truncated source from unresolved glyph coverage, keep ellipsis unmapped, and add ten precondition-asserted field evaluators. Source-only self-review and diff check; not formatted, compiled or executed. No production module wiring or navigation policy adoption.
This commit is contained in:
2026-09-13 02:50:52 -07:00
parent c4a2cf63f1
commit c3ad6f130b
2 changed files with 433 additions and 2 deletions
@@ -18,6 +18,9 @@
//! It measures layout advances, not raster ink; overhang/clip/activation-border //! It measures layout advances, not raster ink; overhang/clip/activation-border
//! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px //! acceptance still requires the real WGPU evaluator. The old 32-grapheme/225px
//! renderer is deliberately untouched for a later baseline comparison. //! renderer is deliberately untouched for a later baseline comparison.
//! Field geometry is a second unwired experiment: exact retained source spans
//! and winning-buffer cluster boxes, with unresolved glyph coverage distinct
//! from truncation. It does not adopt a production navigation policy.
use iced::advanced::{ use iced::advanced::{
graphics::text as graphics_text, graphics::text as graphics_text,
text::{Alignment, LineHeight, Shaping}, text::{Alignment, LineHeight, Shaping},
@@ -94,9 +97,121 @@ pub(super) struct Label {
style: Style, style: Style,
measured_session: RenderSession, measured_session: RenderSession,
measured_zoom: f32, measured_zoom: f32,
// Snapshot of the final displayed Cache entry, after alignment. These are
// cluster advance boxes, never glyph ink or inferred character cells.
clusters: Vec<FieldCluster>,
source_spans: Vec<SourceSpan>,
retained_normalized: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub(super) struct FieldLayout {
pub rectangles: Vec<Rectangle>,
pub covered: Vec<Range<usize>>,
pub truncated: Vec<Range<usize>>,
pub unresolved: Vec<UnresolvedField>,
pub partially_retained: bool,
pub shared_cluster: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum FieldIssue {
MissingGlyph,
NoAdvanceGeometry,
NonPaintingScalar,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct UnresolvedField {
pub source: Range<usize>,
pub reason: FieldIssue,
}
#[derive(Clone, Debug)]
struct SourceSpan {
source: Range<usize>,
normalized: Range<usize>,
whitespace: bool,
non_painting: bool,
}
#[derive(Clone, Debug)]
struct FieldCluster {
normalized: Range<usize>,
rectangles: Vec<Rectangle>,
missing: bool,
} }
impl Label { impl Label {
/// Geometry and coverage are separate: truncation is not missing-font
/// coverage, and a positive .notdef advance is not represented field text.
/// Shared ligatures use whole cluster boxes; no sub-cluster fraction is
/// invented. Returned ranges refer only to the requested original source.
pub fn field_layout(
&self,
session: &RenderSession,
zoom: f32,
position: Point,
field: Range<usize>,
) -> Result<FieldLayout, Error> {
self.check(session, zoom)?;
translated_bounds(position, Size::ZERO)?;
if field.start > field.end || self.source.get(field.clone()).is_none() {
return Err(Error::InvalidFieldRange);
}
let mut result = FieldLayout {
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; }
// 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()
.filter(|(_, cluster)| overlaps(&cluster.normalized, &span.normalized))
.collect();
result.shared_cluster |= matching.iter().any(|(_, cluster)| {
cluster.normalized.end > self.retained_normalized
|| self.source_spans.iter().any(|other| {
overlaps(&other.normalized, &cluster.normalized)
&& !overlaps(&other.source, &field)
})
});
let issue = if span.non_painting {
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()) {
Some(FieldIssue::NoAdvanceGeometry)
} else { None };
if let Some(reason) = issue {
result.unresolved.push(UnresolvedField { source: span.source.clone(), reason });
continue;
}
append_range(&mut result.covered, span.source.clone());
for (index, _) in matching {
selected[index] = true;
}
}
for (cluster, selected) in self.clusters.iter().zip(selected) {
if !selected { continue; }
for bounds in &cluster.rectangles {
let translated = translated_bounds(
Point::new(position.x + bounds.x, position.y + bounds.y), bounds.size(),
)?;
if !result.rectangles.contains(&translated) { result.rectangles.push(translated); }
}
}
result.partially_retained = !result.truncated.is_empty()
&& (!result.covered.is_empty() || !result.unresolved.is_empty());
Ok(result)
}
fn check(&self, session: &RenderSession, zoom: f32) -> Result<(), Error> { fn check(&self, session: &RenderSession, zoom: f32) -> Result<(), Error> {
if !Arc::ptr_eq(&self.measured_session.identity, &session.identity) { if !Arc::ptr_eq(&self.measured_session.identity, &session.identity) {
return Err(Error::StaleRenderSession); return Err(Error::StaleRenderSession);
@@ -194,6 +309,7 @@ pub(super) enum Error {
StaleZoom, StaleZoom,
StaleRenderSession, StaleRenderSession,
FontSetChanged, FontSetChanged,
InvalidFieldRange,
} }
struct Entry { struct Entry {
@@ -286,8 +402,8 @@ impl Cache {
if cuts.len().saturating_sub(1) > MAX_GRAPHEMES { if cuts.len().saturating_sub(1) > MAX_GRAPHEMES {
return Err(Error::GraphemeBudget); return Err(Error::GraphemeBudget);
} }
// Candidate buffers are temporary. Only the resolved strings/metrics // Candidate buffers are temporary. The winning strings, metrics, source
// survive, bounded by MAX_ENTRIES; renderer caching remains independent. // spans and cluster boxes survive; renderer caching remains independent.
let mut shapes = graphics_text::Cache::new(); let mut shapes = graphics_text::Cache::new();
let mut measure = |content: &str| -> Result<Size, Error> { let mut measure = |content: &str| -> Result<Size, Error> {
if content.is_empty() { if content.is_empty() {
@@ -345,6 +461,16 @@ impl Cache {
best best
} }
}; };
let clusters = if displayed.is_empty() {
// 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,
});
field_clusters(entry, zoom)?
};
let label = Arc::new(Label { let label = Arc::new(Label {
source: source.into(), source: source.into(),
displayed, displayed,
@@ -353,6 +479,9 @@ impl Cache {
style, style,
measured_session: session.clone(), measured_session: session.clone(),
measured_zoom: zoom, measured_zoom: zoom,
clusters,
source_spans: normalized.spans,
retained_normalized: retained,
}); });
if self.entries.len() == MAX_ENTRIES { if self.entries.len() == MAX_ENTRIES {
self.entries.pop_front(); self.entries.pop_front();
@@ -394,12 +523,14 @@ fn translated_bounds(position: Point, size: Size) -> Result<Rectangle, Error> {
struct Normalized { struct Normalized {
text: String, text: String,
ends: Vec<(usize, usize)>, ends: Vec<(usize, usize)>,
spans: Vec<SourceSpan>,
} }
impl Normalized { impl Normalized {
fn new(source: &str) -> Self { fn new(source: &str) -> Self {
let mut result = Self { let mut result = Self {
text: String::new(), text: String::new(),
ends: vec![(0, 0)], ends: vec![(0, 0)],
spans: vec![],
}; };
let mut separator = false; let mut separator = false;
for (start, c) in source.char_indices() { for (start, c) in source.char_indices() {
@@ -408,11 +539,23 @@ impl Normalized {
continue; continue;
} }
if separator { if separator {
let normalized_start = result.text.len();
result.text.push(' '); result.text.push(' ');
result.spans.push(SourceSpan {
source: result.ends.last().expect("initial boundary").1..start,
normalized: normalized_start..result.text.len(),
whitespace: true, non_painting: false,
});
result.ends.push((result.text.len(), start)); result.ends.push((result.text.len(), start));
separator = false; separator = false;
} }
let normalized_start = result.text.len();
result.text.push(c); result.text.push(c);
result.spans.push(SourceSpan {
source: start..start + c.len_utf8(),
normalized: normalized_start..result.text.len(),
whitespace: false, non_painting: non_painting_scalar(c),
});
result.ends.push((result.text.len(), start + c.len_utf8())); result.ends.push((result.text.len(), start + c.len_utf8()));
} }
result result
@@ -425,6 +568,64 @@ impl Normalized {
} }
} }
fn overlaps(a: &Range<usize>, b: &Range<usize>) -> bool {
a.start < a.end && b.start < b.end && a.start < b.end && b.start < a.end
}
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.end;
return;
}
ranges.push(next);
}
// Conservative non-membership for controls/default-ignorables, even when a
// shaper assigns them a neighbor's positive-width cluster. Unicode 14 ranges
// 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,
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> {
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,
});
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() {
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); }
}
}
}
Ok(clusters.into_values().collect())
}
#[cfg(test)] #[cfg(test)]
#[path = "scenario_label/tests.rs"] #[path = "scenario_label/tests.rs"]
mod tests; mod tests;
#[cfg(test)]
#[path = "scenario_label/field_tests.rs"]
mod field_tests;
@@ -0,0 +1,230 @@
//! Unwired, unexecuted field-layout falsifiers. Paragraph is an independent
//! shaping oracle; it never invokes the candidate normalization/extraction.
use super::*;
use iced::advanced::{
graphics::text::Paragraph,
text::{Paragraph as _, Text as ParagraphText, Wrapping},
};
fn paragraph(text: &str, zoom: f32, style: Style) -> Paragraph {
Paragraph::with_text(ParagraphText {
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,
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();
(session, label)
}
#[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();
assert!(field.rectangles.is_empty());
assert!(field.covered.is_empty());
assert!(field.unresolved.is_empty());
}
let style = Style::default();
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!(field.covered.is_empty());
assert!(field.rectangles.is_empty());
assert!(!field.truncated.is_empty());
}
#[test]
fn exact_scalar_mapping_does_not_assign_trimmed_or_collapsed_whitespace_to_text() {
let source = " send\t\n assess ";
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();
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();
assert_eq!(result.covered, vec![10..16]);
assert!(!result.rectangles.is_empty());
assert!(result.truncated.is_empty());
assert!(result.unresolved.is_empty());
}
#[test]
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 (session, full) = resolve(source, 900., 1., style);
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();
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();
assert_eq!(result.covered, vec![start..partial.retained_source.end]);
assert_eq!(result.truncated, vec![partial.retained_source.end..source.len()]);
assert!(result.partially_retained);
assert!(result.unresolved.is_empty());
}
#[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 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");
let (session, label) = resolve(source, 900., 1., style);
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!(mixed.covered, vec![0..1, 5..6]);
assert_eq!(mixed.unresolved, missing.unresolved);
assert!(!mixed.rectangles.is_empty());
}
#[test]
fn advanced_ligature_partial_field_uses_whole_shared_cluster_not_fraction() {
let source = "ffi";
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)
.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 (session, label) = resolve(source, 900., 1., style);
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!(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 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");
let (session, label) = resolve(source, 900., 1., style);
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 }]);
}
#[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 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)
.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();
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.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");
}
#[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 oracle = paragraph(source, 1., style);
let run = oracle.buffer().layout_runs().next().unwrap();
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 (session, label) = resolve(source, 900., 1., style);
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");
}
#[test]
fn auto_ascii_to_ellipsis_advanced_transition_uses_final_buffer() {
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!(label.displayed.ends_with('…'));
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)
.expect("final Advanced shape must expose the ligature precondition");
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));
}
#[test]
fn field_geometry_reuses_zoom_session_guards_and_translates_actual_cluster_boxes() {
let source = "Z";
for zoom in [0.5, 1.75] {
let style = Style::default();
let oracle = paragraph(source, zoom, style);
let run = oracle.buffer().layout_runs().next().unwrap();
assert_eq!(run.glyphs.len(), 1);
let glyph = &run.glyphs[0];
assert!(glyph.glyph_id != 0 && glyph.w > 0.);
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));
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));
let empty = label.field_layout(&session, zoom, position, 0..0).unwrap();
assert!(empty.covered.is_empty() && empty.rectangles.is_empty());
}
}