Skip to main content

pdfrum_doc/vt/
hit.rs

1//! Mapping between a point, a caret position and a character index.
2//!
3//! Four pure queries over an already-computed [`Layout`]. Nothing here lays
4//! text out or reads a document; they are searches over the positions
5//! [`crate::vt::layout`] already assigned, which is what keeps the editor
6//! from growing a second layout engine.
7//!
8//! # What a [`Place`] is
9//!
10//! A caret sits **after** a character, and `word == -1` names the position
11//! before the first character of its line. So a line of *n* characters has
12//! *n + 1* caret positions, `-1` through `n - 1`, and the one place that is
13//! not "after some character" is the line header. An empty line has only its
14//! header.
15//!
16//! # The two coordinate spaces
17//!
18//! [`Layout`] positions are y-**down** from the plate's top-left corner.
19//! Callers work in PDF user space, y-up, so [`place_at_point`] and
20//! [`point_at_place`] take and return PDF points and flip on the way in and
21//! out — the same flip [`Layout::to_pdf`] performs, which is why they take
22//! the plate rather than assuming one.
23//!
24//! Between those two spaces sits one further shift, and it is the only thing
25//! the editor adds over the layout: a **vertical alignment offset**. A
26//! single-line field centres its one line in its plate, so the text is drawn
27//! lower than the layout placed it and a click must be lifted by the same
28//! amount before it is searched. Every query here therefore takes an
29//! `offset`, the same `(dx, dy)` `vt::edit_ap::generate` is given
30//! when the appearance is written. Passing `(0.0, 0.0)` is the top-aligned
31//! case. Getting this wrong is not a small error: a centred field's text
32//! sits *below* its layout box, so an unshifted click misses every line and
33//! falls out of the content entirely.
34//!
35//! # The tie-break, which is the whole point of this module
36//!
37//! A click lands after a character when it falls **strictly past that
38//! character's horizontal midpoint**. Strictly: a click exactly on the
39//! midpoint lands *before* the character, so a caret placed by clicking the
40//! exact centre of a glyph goes to its left. Two details make this reproduce
41//! upstream rather than merely resemble it:
42//!
43//! - The midpoint is half the character's **advance**, comb tail included —
44//!   the same `vt::word_width` the layout used — not half its
45//!   inked extent.
46//! - The comparison is a plain `>` on raw floats, with **no epsilon**, unlike
47//!   the vertical comparisons below, which are epsilon-tolerant. That
48//!   asymmetry is upstream's and is deliberate here: it decides which side of
49//!   a boundary a click falls on, and softening it moves carets.
50//!
51//! The vertical searches — which section a point is in, then which line —
52//! use `geom::is_float_bigger` and its sibling, so a point within
53//! `0.0001` of a section or line edge counts as *inside* it.
54//!
55//! # A measured line's size is the layout's, not the caller's request
56//!
57//! [`Config::font_size`] is a **request**, and `0.0` is the request for
58//! automatic sizing. [`crate::vt::layout`] resolves it — picking the largest
59//! step that fits — and records the answer in [`Layout::font_size`]. That
60//! resolved value is what every later measurement reads, so the size that
61//! placed the words is the layout's, and the caller's `Config` still holds
62//! the zero it asked with.
63//!
64//! Every query over a line the layout **has** therefore measures with
65//! [`Layout::font_size`]. Measuring with the config's instead is not a
66//! rounding difference on an auto-sized field, it is **zero**: every advance
67//! collapses, so a caret lands on the leading edge of the character it should
68//! trail and every midpoint becomes the character's own origin. The visible
69//! form on `password` — a field with no `/DA` at all, so the request is zero
70//! and the layout resolves 25 — was a caret at device column 189, the fifth
71//! asterisk's *left* edge, against the oracle's 199. Fixing it moves that row
72//! from 0.991764 to 0.999016 and leaves the other 1703 byte-identical.
73//!
74//! The one place that keeps the config's size is `line_extent`'s **fallback**,
75//! for a place naming a line the layout does not have; its own doc says why,
76//! and the answer there was measured rather than reasoned.
77//!
78//! # Right-to-left runs, where every x reverses
79//!
80//! A right-to-left run is laid out with **descending** x: its first character
81//! in logical order is drawn at the run's right end. Three of this module's
82//! answers turn on that, and each of them is the opposite of its
83//! left-to-right form:
84//!
85//! - **The caret after a character** is that character's *left* edge, not its
86//!   right — its origin, with no advance added. Adding the advance puts every
87//!   caret in the run one character too far right.
88//! - **A line's header** — the position before the first character — is the
89//!   line's *right* edge when that first character is right-to-left, because
90//!   that is where it is drawn.
91//! - **A click** is resolved by a bisection over a predicate that is monotone
92//!   only left to right, so down a run it answers one of the run's two
93//!   logical ends rather than the character nearest the point. A scan would
94//!   answer the run's beginning for every click in it, and the caret would
95//!   never move.
96//!
97//! Each is transcribed at its own function below, with the reason it cannot
98//! be folded into the left-to-right case.
99//!
100//! # Out-of-content points never fail
101//!
102//! A point above every section yields the very first place; one below every
103//! section yields the very last. A point beside a line — left of its first
104//! character or right of its last — is resolved by the same midpoint rule
105//! against that line's own characters, so it clamps to the line's header or
106//! its final character rather than escaping to another line. There is no
107//! "missed" answer, which is what lets a mouse drag off the widget and keep
108//! extending a selection.
109
110// `place` and `plate` are one letter apart and both are the domain's own
111// words: a `Place` is a caret position, and the plate is the box the text is
112// set into. Renaming either to satisfy the lint would make this module read
113// against the spec that names them.
114#![allow(clippy::similar_names)]
115
116use kurbo::{Point, Rect};
117
118use crate::geom;
119use crate::vt::{Config, Layout, Metrics, Section, word_width};
120
121/// A caret position: after character `word` of line `line` of section
122/// `section`.
123///
124/// `word == None` is the line header — the position before the line's first
125/// character. Every other value names the character the caret sits after.
126///
127/// ```
128/// use pdfrum_doc::geom;
129/// use pdfrum_doc::vt::{Config, Metrics, layout};
130///
131/// // Every character ten thousandths wide: the stub the layout assertions
132/// // in this crate are written against.
133/// let width = |_code: u32| 10;
134/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
135///
136/// let config = Config {
137///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
138///     font_size: 10.0,
139///     multi_line: true,
140///     ..Config::default()
141/// };
142/// let laid_out = layout("Hi", &config, &metrics);
143/// use pdfrum_doc::vt::hit;
144/// use pdfrum_doc::vt::hit::Place;
145///
146/// // The header -- the position before a line's first character --
147/// // orders ahead of every character on it.
148/// assert!(Place::new(0, 0, None) < Place::new(0, 0, Some(0)));
149/// ```
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
151pub struct Place {
152    /// Which paragraph.
153    pub section: u32,
154    /// Which line of that paragraph.
155    pub line: u32,
156    /// The character the caret follows, or `None` for the line header.
157    ///
158    /// `None` orders before every `Some`, so the header sorts ahead of the
159    /// line's characters.
160    pub word: Option<u32>,
161}
162
163impl Place {
164    /// A place, from the three indices.
165    ///
166    /// ```
167    /// use pdfrum_doc::vt::hit::Place;
168    ///
169    /// let after_first = Place::new(0, 0, Some(0));
170    /// assert_eq!(after_first.word, Some(0));
171    /// ```
172    #[must_use]
173    pub fn new(section: u32, line: u32, word: Option<u32>) -> Place {
174        Place {
175            section,
176            line,
177            word,
178        }
179    }
180
181    /// The place every layout begins at: the header of its first line.
182    ///
183    /// A constant rather than a query, because it does not depend on the
184    /// layout — the first caret position is the first line's header whatever
185    /// the text is, including no text at all. [`begin_place`] is the same
186    /// value, spelled for symmetry with [`end_place`], which does need the
187    /// layout.
188    ///
189    /// The header is `None`, not a sentinel index.
190    ///
191    /// ```
192    /// use pdfrum_doc::vt::hit::Place;
193    ///
194    /// // The beginning of the text, not `(0, 0, 0)`: a zero `word` is the
195    /// // position *after* the first character.
196    /// assert_eq!(Place::START, Place::new(0, 0, None));
197    /// assert_eq!(Place::default(), Place::START);
198    /// ```
199    pub const START: Place = Place {
200        section: 0,
201        line: 0,
202        word: None,
203    };
204
205    /// [`Place::START`], as a function.
206    ///
207    /// ```
208    /// use pdfrum_doc::vt::hit::Place;
209    ///
210    /// assert_eq!(Place::start(), Place::START);
211    /// ```
212    #[must_use]
213    pub fn start() -> Place {
214        Place::START
215    }
216}
217
218/// The beginning of the text, not `(0, 0, 0)`.
219///
220/// `#[derive(Default)]` would zero all three fields, and a zero `word` is the
221/// position *after* the first character — a different, valid-looking caret
222/// that no layout ever means by "the default". The beginning of the text is
223/// section 0, line 0, and **no** word, which is what a caller reaching for a
224/// default wants.
225impl Default for Place {
226    fn default() -> Place {
227        Place::START
228    }
229}
230
231/// The first place in a layout: the header of its first line.
232///
233/// ```
234/// use pdfrum_doc::geom;
235/// use pdfrum_doc::vt::{Config, Metrics, layout};
236///
237/// // Every character ten thousandths wide: the stub the layout assertions
238/// // in this crate are written against.
239/// let width = |_code: u32| 10;
240/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
241///
242/// let config = Config {
243///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
244///     font_size: 10.0,
245///     multi_line: true,
246///     ..Config::default()
247/// };
248/// let laid_out = layout("Hi", &config, &metrics);
249/// use pdfrum_doc::vt::hit;
250/// use pdfrum_doc::vt::hit::Place;
251///
252/// assert_eq!(hit::begin_place(&laid_out), Place::START);
253/// ```
254#[must_use]
255pub fn begin_place(layout: &Layout) -> Place {
256    let _ = layout;
257    Place::start()
258}
259
260/// The last place in a layout: after the last character of its last line.
261///
262/// An empty layout, and one whose last section has no lines, both answer the
263/// header of section zero — there is nowhere else for a caret to be.
264///
265/// ```
266/// use pdfrum_doc::geom;
267/// use pdfrum_doc::vt::{Config, Metrics, layout};
268///
269/// // Every character ten thousandths wide: the stub the layout assertions
270/// // in this crate are written against.
271/// let width = |_code: u32| 10;
272/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
273///
274/// let config = Config {
275///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
276///     font_size: 10.0,
277///     multi_line: true,
278///     ..Config::default()
279/// };
280/// let laid_out = layout("Hi", &config, &metrics);
281/// use pdfrum_doc::vt::hit;
282/// use pdfrum_doc::vt::hit::Place;
283///
284/// // After the last character of the last line.
285/// assert_eq!(hit::end_place(&laid_out), Place::new(0, 0, Some(1)));
286/// // An empty layout answers the header of section zero.
287/// assert_eq!(hit::end_place(&Default::default()), Place::new(0, 0, None));
288/// ```
289#[must_use]
290pub fn end_place(layout: &Layout) -> Place {
291    let Some(section) = layout.sections.len().checked_sub(1) else {
292        return Place::new(0, 0, None);
293    };
294    let Some(last) = layout.sections.get(section) else {
295        return Place::new(0, 0, None);
296    };
297    end_of_section(last, section)
298}
299
300/// The last place of one section.
301fn end_of_section(section: &Section, index: usize) -> Place {
302    let Some(line_index) = section.lines.len().checked_sub(1) else {
303        return Place::new(clamp_index(index), 0, None);
304    };
305    let word = section
306        .lines
307        .get(line_index)
308        .and_then(crate::vt::Line::last_word);
309    Place::new(clamp_index(index), clamp_index(line_index), word)
310}
311
312/// A `usize` index as the `u32` a [`Place`] holds, saturating rather than
313/// wrapping on a layout too large to index — such a layout cannot be produced
314/// by this crate, and saturating keeps the query total.
315fn clamp_index(index: usize) -> u32 {
316    u32::try_from(index).unwrap_or(u32::MAX)
317}
318
319/// The caret position a click at `point` selects.
320///
321/// `point` is in PDF user space (y-up); `plate` is the box the layout was set
322/// into, the same one [`Config::plate`] carried. The result is always a valid
323/// place — see the module docs on out-of-content points.
324///
325/// # The rule
326///
327/// Within a line, the caret lands after the last character whose horizontal
328/// midpoint the point is **strictly** past. A point exactly on a midpoint is
329/// therefore *before* that character, not after it.
330///
331/// ```
332/// use pdfrum_doc::geom;
333/// use pdfrum_doc::vt::{Config, Metrics, layout};
334///
335/// // Every character ten thousandths wide: the stub the layout assertions
336/// // in this crate are written against.
337/// let width = |_code: u32| 10;
338/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
339///
340/// let config = Config {
341///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
342///     font_size: 10.0,
343///     multi_line: true,
344///     ..Config::default()
345/// };
346/// let laid_out = layout("Hi", &config, &metrics);
347/// use pdfrum_doc::vt::hit;
348///
349/// // A point left of the first character lands on the line header.
350/// let point = kurbo::Point::new(-10.0, 35.0);
351/// let place = hit::place_at_point(
352///     &laid_out, config.plate, &config, &metrics, (0.0, 0.0), point);
353/// assert_eq!(place.word, None);
354/// ```
355#[must_use]
356pub fn place_at_point(
357    layout: &Layout,
358    plate: Rect,
359    config: &Config,
360    metrics: &Metrics<'_>,
361    offset: (f32, f32),
362    point: Point,
363) -> Place {
364    // Into layout space: undo the alignment offset the text was drawn with,
365    // then x from the plate's left edge and y downward from its top. The
366    // same transform `point_at_place` inverts.
367    #[allow(clippy::cast_possible_truncation)]
368    let x = point.x as f32 - offset.0 - geom::left(plate);
369    #[allow(clippy::cast_possible_truncation)]
370    let y = geom::top(plate) - (point.y as f32 - offset.1);
371
372    match find_section(layout, y) {
373        Found::Inside(index, section) => {
374            let mut place = place_in_section(section, config, metrics, layout.font_size, x, y);
375            place.section = clamp_index(index);
376            place
377        }
378        Found::Above => begin_place(layout),
379        Found::Below => end_place(layout),
380    }
381}
382
383/// Which section a y falls in, or which way it fell off.
384enum Found<'a> {
385    /// Inside this section, at this index.
386    Inside(usize, &'a Section),
387    /// Above every section.
388    Above,
389    /// Below every section, or there are none.
390    Below,
391}
392
393/// Locates the section a layout-space y sits in.
394///
395/// Sections are laid out top to bottom and never overlap, so this is a scan
396/// for the first whose box contains the point, with the two off-the-end
397/// answers distinguished. A point in the seam between two sections — which
398/// the epsilon makes possible — belongs to the first, because the scan stops
399/// at the first match.
400fn find_section(layout: &Layout, y: f32) -> Found<'_> {
401    let mut any_above = false;
402    for (index, section) in layout.sections.iter().enumerate() {
403        let (top, bottom) = (geom::bottom(section.rect), geom::top(section.rect));
404        if geom::is_float_smaller(y, top) {
405            // Above this section. Since sections only descend from here, the
406            // point is above everything unless an earlier one already claimed
407            // to be above the point.
408            return if any_above {
409                Found::Below
410            } else {
411                Found::Above
412            };
413        }
414        if geom::is_float_bigger(y, bottom) {
415            any_above = true;
416            continue;
417        }
418        return Found::Inside(index, section);
419    }
420    Found::Below
421}
422
423/// The place a point selects within one section, in layout space.
424///
425/// `font_size` is the **layout's** resolved size, not the config's request —
426/// see the module docs.
427fn place_in_section(
428    section: &Section,
429    config: &Config,
430    metrics: &Metrics<'_>,
431    font_size: f32,
432    x: f32,
433    y: f32,
434) -> Place {
435    let Some((index, line)) = find_line(section, y) else {
436        // Above or below every line of a section the point is nonetheless
437        // inside. Falling to the section's own ends keeps the answer a valid
438        // place; an empty section has only its header.
439        return if section.lines.is_empty() {
440            Place::new(0, 0, None)
441        } else {
442            end_of_section(section, 0)
443        };
444    };
445    Place::new(
446        0,
447        clamp_index(index),
448        word_at_x(section, line_range(line), config, metrics, font_size, x),
449    )
450}
451
452/// Locates the line a section-space y sits in.
453///
454/// A y above the first line answers the first line, and one below the last
455/// answers the last — clicking in a field's top or bottom margin puts the
456/// caret on the nearest line rather than nowhere.
457fn find_line(section: &Section, y: f32) -> Option<(usize, &crate::vt::Line)> {
458    let mut last: Option<(usize, &crate::vt::Line)> = None;
459    for (index, line) in section.lines.iter().enumerate() {
460        // A line's box runs from its baseline less its ascent to its baseline
461        // less its descent — descent being negative, so the bottom is below.
462        let top = line.y - line.ascent;
463        let bottom = line.y - line.descent;
464        if geom::is_float_smaller(y, top) {
465            // Above this line. The first line claims a point above the whole
466            // section; otherwise the point sits in the gap above this one and
467            // the line before it is the nearer.
468            return Some(last.unwrap_or((index, line)));
469        }
470        if geom::is_float_bigger(y, bottom) {
471            last = Some((index, line));
472            continue;
473        }
474        return Some((index, line));
475    }
476    last
477}
478
479/// The half-open range of word indices one line covers.
480///
481/// A line whose `words` is `None` is the single line of an empty section and
482/// covers nothing.
483fn line_range(line: &crate::vt::Line) -> std::ops::Range<usize> {
484    let Some(words) = line.words.clone() else {
485        return 0..0;
486    };
487    let begin = usize::try_from(words.start).unwrap_or(usize::MAX);
488    let end = usize::try_from(words.end).unwrap_or(usize::MAX);
489    begin..end.max(begin)
490}
491
492/// Which character of a line's range a layout-space x lands after.
493///
494/// Returns `None` for the line header. This is the tie-break the module docs
495/// describe: strictly past a character's midpoint puts the caret after it.
496///
497/// # Why this bisects rather than scans
498///
499/// The predicate — past the midpoint — is monotone along a left-to-right
500/// line, and a scan and a bisection agree on every monotone input. A
501/// right-to-left run is **not** monotone: the layout gives its characters
502/// descending x, so the first character's midpoint is the rightmost one on
503/// the line and a scan stops at it, answering the header for every click left
504/// of it. Upstream bisects anyway and takes whatever the narrowing lands on,
505/// which for a descending run resolves toward the run's *last* logical
506/// character — the one drawn at the line's left edge — and so puts the caret
507/// at the end of the run rather than at its beginning.
508///
509/// That is not an incidental difference. A click in the left half of a Hebrew
510/// word is the common case, and the two answers name opposite ends of it. So
511/// the bisection is transcribed rather than simplified, including the three
512/// details that decide where it lands: `nMid` is recomputed from the halved
513/// interval each step, the two `nMid == nLeft` / `nMid == nRight` guards are
514/// what terminate it, and **the answer is the post-loop test at `nMid`
515/// alone** — not the running maximum a scan would keep.
516fn word_at_x(
517    section: &Section,
518    range: std::ops::Range<usize>,
519    config: &Config,
520    metrics: &Metrics<'_>,
521    font_size: f32,
522    x: f32,
523) -> Option<u32> {
524    // Raw `>`, no epsilon: the boundary belongs to the character's left half,
525    // so a click exactly on a midpoint lands before that character.
526    //
527    // `font_size` is the layout's, so an auto-sized field's midpoints are the
528    // real ones. Measured with the config's request instead, every advance is
529    // zero and every midpoint collapses onto the character's own origin —
530    // which makes the bisection answer by position alone and puts the caret
531    // one character early everywhere in the line.
532    let past_midpoint = |index: usize| {
533        section
534            .words
535            .get(index)
536            .is_some_and(|word| x > word.x + word_width(word, config, metrics, font_size) * 0.5)
537    };
538
539    if range.is_empty() {
540        return None;
541    }
542    let (mut left, mut right) = (range.start, range.end);
543    let mut mid = left.saturating_add(right) / 2;
544    while left < right {
545        if mid == left {
546            break;
547        }
548        if mid == right {
549            mid = mid.saturating_sub(1);
550            break;
551        }
552        if section.words.get(mid).is_none() {
553            break;
554        }
555        if past_midpoint(mid) {
556            left = mid;
557        } else {
558            right = mid;
559        }
560        mid = left.saturating_add(right) / 2;
561    }
562    if past_midpoint(mid) {
563        // A `mid` too large to be a `u32` cannot be produced by this crate,
564        // and saturating keeps the query total. `Option` keeps an
565        // unrepresentable index apart from "before the first character".
566        return Some(u32::try_from(mid).unwrap_or(u32::MAX));
567    }
568    None
569}
570
571/// Where a caret at `place` is drawn, in PDF user space.
572///
573/// The returned point is the caret's **top**: `x` is the trailing edge of the
574/// character the place names (its leading edge, for a line header), and `y`
575/// is the line's ascent line. [`caret_rect`] turns that into the rectangle
576/// [`crate::ap::field_body::Highlight`] wants.
577///
578/// A place naming a section, line or character the layout does not have
579/// answers that container's own start rather than failing — the layout can
580/// shrink under an edit while a caret still points into it, and a caret
581/// outside the text is not an error.
582///
583/// ```
584/// use pdfrum_doc::geom;
585/// use pdfrum_doc::vt::{Config, Metrics, layout};
586///
587/// // Every character ten thousandths wide: the stub the layout assertions
588/// // in this crate are written against.
589/// let width = |_code: u32| 10;
590/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
591///
592/// let config = Config {
593///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
594///     font_size: 10.0,
595///     multi_line: true,
596///     ..Config::default()
597/// };
598/// let laid_out = layout("Hi", &config, &metrics);
599/// use pdfrum_doc::vt::hit;
600/// use pdfrum_doc::vt::hit::Place;
601///
602/// // The inverse of `place_at_point`, in PDF user space.
603/// let point = hit::point_at_place(
604///     &laid_out, config.plate, &config, &metrics, (0.0, 0.0), Place::START);
605/// assert_eq!(point.x, f64::from(geom::left(config.plate)));
606/// ```
607#[must_use]
608pub fn point_at_place(
609    layout: &Layout,
610    plate: Rect,
611    config: &Config,
612    metrics: &Metrics<'_>,
613    offset: (f32, f32),
614    place: Place,
615) -> Point {
616    let (x, y) = caret_position(layout, config, metrics, place);
617    let (x, y) = Layout::to_pdf(plate, x, y);
618    Point::new(f64::from(x + offset.0), f64::from(y + offset.1))
619}
620
621/// The caret's rectangle: a hairline from the line's ascent to its descent.
622///
623/// `width` is the caret's thickness — [`crate::ap::field_body::CARET_WIDTH`]
624/// for a drawn caret. The rectangle is in PDF user space, ready to hand to
625/// [`crate::ap::field_body::Highlight`].
626///
627/// ```
628/// use pdfrum_doc::geom;
629/// use pdfrum_doc::vt::{Config, Metrics, layout};
630///
631/// // Every character ten thousandths wide: the stub the layout assertions
632/// // in this crate are written against.
633/// let width = |_code: u32| 10;
634/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
635///
636/// let config = Config {
637///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
638///     font_size: 10.0,
639///     multi_line: true,
640///     ..Config::default()
641/// };
642/// let laid_out = layout("Hi", &config, &metrics);
643/// use pdfrum_doc::vt::hit;
644/// use pdfrum_doc::vt::hit::Place;
645///
646/// let rect = hit::caret_rect(
647///     &laid_out, config.plate, &config, &metrics, (0.0, 0.0), Place::START, 1.0);
648/// // A hairline from the line's ascent down to its descent.
649/// assert_eq!(geom::width(rect), 1.0);
650/// assert!(geom::height(rect) > 0.0);
651/// ```
652#[must_use]
653pub fn caret_rect(
654    layout: &Layout,
655    plate: Rect,
656    config: &Config,
657    metrics: &Metrics<'_>,
658    offset: (f32, f32),
659    place: Place,
660    width: f32,
661) -> Rect {
662    let (x, top) = caret_position(layout, config, metrics, place);
663    let height = line_extent(layout, config, metrics, place);
664    let (left, top) = Layout::to_pdf(plate, x, top);
665    let (left, top) = (left + offset.0, top + offset.1);
666    geom::rect(left, top - height, left + width, top)
667}
668
669/// The caret's x and its line's ascent line, both in layout space.
670fn caret_position(
671    layout: &Layout,
672    config: &Config,
673    metrics: &Metrics<'_>,
674    place: Place,
675) -> (f32, f32) {
676    let Some(section) = layout.sections.get(place.section as usize) else {
677        return (0.0, 0.0);
678    };
679    let Some(line) = section.lines.get(place.line as usize) else {
680        return (0.0, 0.0);
681    };
682    let top = line.y - line.ascent;
683    let Some(word_index) = place.word else {
684        return (line_caret_x(section, line), top);
685    };
686    let index = usize::try_from(word_index).unwrap_or(usize::MAX);
687    let Some(word) = section.words.get(index) else {
688        return (line_caret_x(section, line), top);
689    };
690    (caret_x(word, config, metrics, layout.font_size), top)
691}
692
693/// The x a caret sitting **after** one character is drawn at.
694///
695/// A left-to-right character's trailing edge is its right one, so the caret is
696/// the character's origin plus its advance. A right-to-left character advances
697/// the other way — its origin is already the *right* end of the cell the layout
698/// gave it, and the caret after it belongs on its **left**, which is the
699/// origin itself. Adding the advance to both is the same one-advance error
700/// everywhere it happens: every caret in a Hebrew or Arabic run lands one
701/// character to the right of the gap it names.
702///
703/// The advance is measured at the **layout's** resolved `font_size`. At the
704/// config's request it would be zero on an auto-sized field, which collapses
705/// the left-to-right branch onto the right-to-left one: every caret would sit
706/// on its character's leading edge, one advance short of the gap it names.
707fn caret_x(word: &crate::vt::Word, config: &Config, metrics: &Metrics<'_>, font_size: f32) -> f32 {
708    if word.is_rtl {
709        word.x
710    } else {
711        word.x + word_width(word, config, metrics, font_size)
712    }
713}
714
715/// The x a caret sitting at a line's **header** — before its first character —
716/// is drawn at.
717///
718/// The header is the position before the first character *in logical order*,
719/// and in a right-to-left line that character is drawn at the line's right
720/// end. So the answer is the line's trailing edge, not its leading one: a
721/// left-to-right line answers its left edge and a right-to-left line answers
722/// `x + width`. An empty line has no first character and keeps the left edge,
723/// which is what puts an empty right-aligned field's caret where its
724/// alignment already put the line.
725///
726/// The direction comes from the **first word alone**, not from the line's
727/// dominant direction: a line whose first run is right-to-left and whose
728/// second is not still answers its right edge.
729fn line_caret_x(section: &Section, line: &crate::vt::Line) -> f32 {
730    let Some(words) = line.words.clone() else {
731        return line.x;
732    };
733    // Only a word this line actually owns decides: a `start` past the
734    // section's words is a layout that shrank under an edit, and it answers
735    // the left edge rather than reading a neighbour's direction. An empty
736    // range owns nothing.
737    let first = (words.start < words.end)
738        .then(|| usize::try_from(words.start).ok())
739        .flatten()
740        .and_then(|index| section.words.get(index));
741    if first.is_some_and(|word| word.is_rtl) {
742        line.x + line.width
743    } else {
744        line.x
745    }
746}
747
748/// The height of the line a place sits on.
749///
750/// # The fallback deliberately keeps the **config's** size
751///
752/// It is reached only for a place naming a line the layout does not have, and
753/// the two sizes then say different things. `Layout::font_size` is what the
754/// resolved layout used, and on an auto-sized field it answers a height for a
755/// line that does not exist; the config's is what the *caller* asked for, and
756/// a caller that asked for automatic sizing gets nothing. That second answer
757/// is upstream's: `CPWL_EditImpl::GetCaretRect` reads a word range that is
758/// empty for a missing place, so the caret it hands back has no height.
759///
760/// This was measured, not reasoned. Substituting the layout's size here moves
761/// **170 rows, 37 of them downward** — every widget whose caret rect is asked
762/// for a line it does not have gains a height it did not have before, and the
763/// checkbox and radio families lose by it. The upward move on `password` is
764/// +0.000006 of the +0.007252 this module's other two size fixes are worth.
765/// So this one stays as it is; the two that matter are `caret_x` and
766/// `word_at_x`, and both take the layout's size for a line that does exist.
767fn line_extent(layout: &Layout, config: &Config, metrics: &Metrics<'_>, place: Place) -> f32 {
768    let fallback = || {
769        crate::vt::font_ascent(metrics, config.font_size)
770            - crate::vt::font_descent(metrics, config.font_size)
771    };
772    layout
773        .sections
774        .get(place.section as usize)
775        .and_then(|section| section.lines.get(place.line as usize))
776        .map_or_else(fallback, |line| line.ascent - line.descent)
777}
778
779/// How many characters precede `place` in the layout's text.
780///
781/// Counting the way the text itself does: every character of every earlier
782/// section, **plus one for each section break**, plus the characters of this
783/// place's own line up to and including the one it names. So the index of the
784/// caret before the first character is zero, and the index after the last is
785/// the text's length.
786///
787/// # A wrapped line's header is not a position of its own
788///
789/// `place_at_point` names the *line header* — a place with no word — for a
790/// click left of every midpoint on a line, and on a wrapped line that header
791/// is the same caret position as the end of the line above it. The two are
792/// folded together before anything is counted: a header on any line but the
793/// first becomes the place of the character preceding it. So the header of
794/// line 1 of a section whose first line holds characters `0..=4` counts as
795/// **5**, not 0. Skipping the fold is not a rounding difference: it sends a
796/// caret clicked at the start of a wrapped line to the start of the whole
797/// field, and the next keystroke lands there.
798///
799/// # A place past the layout
800///
801/// Clamped to the end, and the clamp counts **no trailing section break**, so
802/// an out-of-range section index yields the last section's end rather than
803/// one past it.
804///
805/// ```
806/// use pdfrum_doc::geom;
807/// use pdfrum_doc::vt::{Config, Metrics, layout};
808///
809/// // Every character ten thousandths wide: the stub the layout assertions
810/// // in this crate are written against.
811/// let width = |_code: u32| 10;
812/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
813///
814/// let config = Config {
815///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
816///     font_size: 10.0,
817///     multi_line: true,
818///     ..Config::default()
819/// };
820/// let laid_out = layout("Hi", &config, &metrics);
821/// use pdfrum_doc::vt::hit;
822/// use pdfrum_doc::vt::hit::Place;
823///
824/// assert_eq!(hit::word_index_of_place(&laid_out, Place::START), 0);
825/// assert_eq!(hit::word_index_of_place(&laid_out, Place::new(0, 0, Some(1))), 2);
826/// ```
827#[must_use]
828pub fn word_index_of_place(layout: &Layout, place: Place) -> usize {
829    // The fold, before anything is counted. A header on the first line has no
830    // line above it and stays where it is.
831    if place.word.is_none() && place.line > 0 {
832        // The character before this line's first is the last of the line
833        // above -- which is where a header on any line but the first folds to.
834        let previous = layout
835            .sections
836            .get(place.section as usize)
837            .and_then(|section| section.lines.get(place.line as usize))
838            .map(|line| {
839                let before = line
840                    .words
841                    .as_ref()
842                    .and_then(|words| words.start.checked_sub(1));
843                Place::new(place.section, place.line - 1, before)
844            });
845        if let Some(previous) = previous {
846            return word_index_of_place(layout, previous);
847        }
848    }
849
850    let target = place.section as usize;
851    let last = layout.sections.len().saturating_sub(1);
852    let mut index: usize = 0;
853    for (position, section) in layout.sections.iter().enumerate() {
854        if position >= target {
855            break;
856        }
857        index = index.saturating_add(section.words.len());
858        // The break between this section and the next counts as one
859        // character, exactly as the tokenizer counted it on the way in — and
860        // there is no break after the last section to count.
861        if position != last {
862            index = index.saturating_add(SECTION_BREAK_LENGTH);
863        }
864    }
865    let Some(section) = layout.sections.get(target) else {
866        return index;
867    };
868    // The header is "before character 0", so it consumes none of this
869    // section; character `w` consumes `w + 1`.
870    let after = place.word.map_or(0, |word| {
871        usize::try_from(word)
872            .unwrap_or(usize::MAX)
873            .saturating_add(1)
874    });
875    index.saturating_add(after.min(section.words.len()))
876}
877
878/// A section break counts as one character in the flat index, the way the
879/// line break it came from did.
880const SECTION_BREAK_LENGTH: usize = 1;
881
882/// Which caret position sits after `index` characters of the layout's text.
883///
884/// The inverse of [`word_index_of_place`]: for every place a layout can
885/// produce, `place_of_word_index(layout, word_index_of_place(layout, p)) ==
886/// p`. An index past the end answers the last place.
887///
888/// An index landing exactly *on* a section break — the position between a
889/// paragraph's last character and the next paragraph's first — answers the
890/// **end of the earlier** section, which is where a caret typed to that point
891/// actually sits.
892///
893/// ```
894/// use pdfrum_doc::geom;
895/// use pdfrum_doc::vt::{Config, Metrics, layout};
896///
897/// // Every character ten thousandths wide: the stub the layout assertions
898/// // in this crate are written against.
899/// let width = |_code: u32| 10;
900/// let metrics = Metrics { width: &width, ascent: 1000, descent: -200 };
901///
902/// let config = Config {
903///     plate: geom::rect(0.0, 0.0, 100.0, 40.0),
904///     font_size: 10.0,
905///     multi_line: true,
906///     ..Config::default()
907/// };
908/// let laid_out = layout("Hi", &config, &metrics);
909/// use pdfrum_doc::vt::hit;
910/// use pdfrum_doc::vt::hit::Place;
911///
912/// // The inverse of `word_index_of_place`.
913/// assert_eq!(hit::place_of_word_index(&laid_out, 0), Place::START);
914/// assert_eq!(hit::place_of_word_index(&laid_out, 2), Place::new(0, 0, Some(1)));
915/// ```
916#[must_use]
917pub fn place_of_word_index(layout: &Layout, index: usize) -> Place {
918    let mut consumed: usize = 0;
919    for (position, section) in layout.sections.iter().enumerate() {
920        let end = consumed.saturating_add(section.words.len());
921        if index <= end {
922            let within = index.saturating_sub(consumed);
923            // Index 0 within a section is its header; index `n` is character
924            // `n - 1`.
925            let word = within
926                .checked_sub(1)
927                .map(|w| u32::try_from(w).unwrap_or(u32::MAX));
928            return place_of_word(section, position, word);
929        }
930        consumed = end.saturating_add(SECTION_BREAK_LENGTH);
931    }
932    end_place(layout)
933}
934
935/// The place naming character `word` of a section, with the line it falls on
936/// resolved.
937fn place_of_word(section: &Section, index: usize, word: Option<u32>) -> Place {
938    let line = line_of_word(section, word);
939    Place::new(clamp_index(index), line, word)
940}
941
942/// Which line of a section a character index falls on.
943///
944/// A `None` word is the first line's header. A character past the section's
945/// last line answers that last line, which keeps the place valid.
946fn line_of_word(section: &Section, word: Option<u32>) -> u32 {
947    let Some(word) = word else {
948        return 0;
949    };
950    for (index, line) in section.lines.iter().enumerate() {
951        if line
952            .words
953            .as_ref()
954            .is_some_and(|words| words.contains(&word))
955        {
956            return clamp_index(index);
957        }
958    }
959    clamp_index(section.lines.len().saturating_sub(1))
960}
961
962#[cfg(test)]
963mod tests {
964    use super::{
965        Place, begin_place, caret_rect, end_place, place_at_point, place_of_word_index,
966        point_at_place, word_index_of_place,
967    };
968
969    /// The caret positions of a line holding `n` characters: the header
970    /// first, then each character.
971    fn carets(n: u32) -> impl Iterator<Item = Option<u32>> {
972        std::iter::once(None).chain((0..n).map(Some))
973    }
974    use crate::geom;
975    use crate::vt::{self, Config, Metrics};
976    use kurbo::Point;
977
978    /// A single-line plate a hundred wide and thirty tall, the shape
979    /// `text_form.pdf`'s field has.
980    fn config() -> Config {
981        Config {
982            plate: geom::rect(101.0, 101.0, 199.0, 129.0),
983            font_size: 12.0,
984            ..Config::default()
985        }
986    }
987
988    /// Helvetica's advance widths, per mille, for the characters the tests
989    /// use. Hand-transcribed so the arithmetic below can be checked by hand
990    /// rather than against whatever the font loader answers.
991    fn helvetica(ch: u32) -> i32 {
992        match char::from_u32(ch) {
993            Some('A' | 'B' | 'E') => 667,
994            Some('C' | 'D' | 'H') => 722,
995            Some('F') => 611,
996            Some('G') => 778,
997            Some(' ') => 278,
998            _ => 556,
999        }
1000    }
1001
1002    fn metrics() -> Metrics<'static> {
1003        Metrics {
1004            width: &helvetica,
1005            ascent: 723,
1006            descent: -207,
1007        }
1008    }
1009
1010    /// Every character ten wide, which makes midpoints land on fives.
1011    fn ten(_: u32) -> i32 {
1012        10
1013    }
1014
1015    fn tens() -> Metrics<'static> {
1016        Metrics {
1017            width: &ten,
1018            ascent: 1000,
1019            descent: -200,
1020        }
1021    }
1022
1023    /// The offset a single-line field is drawn with: its one line centred in
1024    /// its plate. The same expression `ap::field_body::vertical_offset`
1025    /// computes, restated here so the tests do not depend on that module.
1026    fn centred(layout: &vt::Layout, config: &Config) -> (f32, f32) {
1027        let content = layout.content_rect_pdf(config.plate);
1028        (
1029            0.0,
1030            (geom::height(content) - geom::height(config.plate)) * 0.5,
1031        )
1032    }
1033
1034    fn at(config: &Config, metrics: &Metrics<'_>, text: &str, x: f32, y: f32) -> Place {
1035        let layout = vt::layout(text, config, metrics);
1036        let offset = centred(&layout, config);
1037        place_at_point(
1038            &layout,
1039            config.plate,
1040            config,
1041            metrics,
1042            offset,
1043            Point::new(f64::from(x), f64::from(y)),
1044        )
1045    }
1046
1047    /// The acceptance geometry: `text_form.pdf`'s field, Helvetica 12, the
1048    /// eight characters the embeddertest types, and the click it makes.
1049    ///
1050    /// The field's `/Rect` is `[100 100 200 130]` and its default border is
1051    /// one unit, so the plate starts at x = 101. Helvetica 12 sets
1052    /// `ABCDEFGH` at these advances:
1053    ///
1054    /// ```text
1055    ///   A 101.000 .. 109.004   midpoint 105.002
1056    ///   B 109.004 .. 117.008   midpoint 113.006
1057    ///   C 117.008 .. 125.672   midpoint 121.340
1058    ///   D 125.672 .. 134.336   midpoint 130.004
1059    ///   E 134.336 .. 142.340   midpoint 138.338
1060    /// ```
1061    ///
1062    /// A click at x = 134 is past D's midpoint and short of E's, so the caret
1063    /// lands after D — after exactly four characters, which is what
1064    /// `InsertTextInPopulatedTextFieldMiddle` observes when it inserts
1065    /// `Hello` and reads back `ABCDHelloEFGH`.
1066    #[test]
1067    fn the_embeddertests_middle_click_lands_after_four_characters() {
1068        let config = config();
1069        let metrics = metrics();
1070        let layout = vt::layout("ABCDEFGH", &config, &metrics);
1071        let place = place_at_point(
1072            &layout,
1073            config.plate,
1074            &config,
1075            &metrics,
1076            centred(&layout, &config),
1077            Point::new(134.0, 115.0),
1078        );
1079        assert_eq!(place.word, Some(3), "the caret sits after D");
1080        assert_eq!(
1081            word_index_of_place(&layout, place),
1082            4,
1083            "four characters precede the caret"
1084        );
1085    }
1086
1087    /// The two neighbouring embeddertests, which calibrate the same rule at
1088    /// the ends: a click at the field's left edge inserts in front of
1089    /// everything, and one at x = 166 inserts behind it.
1090    #[test]
1091    fn the_same_field_clicked_at_either_end_gives_the_two_extremes() {
1092        let config = config();
1093        let metrics = metrics();
1094        let layout = vt::layout("ABCDEFGH", &config, &metrics);
1095        let offset = centred(&layout, &config);
1096        let begin = place_at_point(
1097            &layout,
1098            config.plate,
1099            &config,
1100            &metrics,
1101            offset,
1102            Point::new(102.0, 115.0),
1103        );
1104        assert_eq!(begin.word, None, "before the first character");
1105        assert_eq!(word_index_of_place(&layout, begin), 0);
1106
1107        let end = place_at_point(
1108            &layout,
1109            config.plate,
1110            &config,
1111            &metrics,
1112            offset,
1113            Point::new(166.0, 115.0),
1114        );
1115        assert_eq!(end.word, Some(7), "after the last character");
1116        assert_eq!(word_index_of_place(&layout, end), 8);
1117    }
1118
1119    /// The tie-break itself, on a plate where the midpoints are round
1120    /// numbers. Every character is ten wide, so the first sits at 101..111
1121    /// with its midpoint at 106.
1122    #[test]
1123    fn a_click_exactly_on_a_midpoint_lands_before_that_character() {
1124        let config = config();
1125        let metrics = tens();
1126        // Ten wide at size twelve is 10 * 12 * 0.001 = 0.12 per character,
1127        // so use a size that makes the arithmetic whole.
1128        let config = Config {
1129            font_size: 1000.0,
1130            ..config
1131        };
1132        // Each character is now ten units wide: 101..111, 111..121, ...
1133        assert_eq!(at(&config, &metrics, "abc", 106.0, 115.0).word, None);
1134        // A hair past it, and the caret moves after the character.
1135        assert_eq!(at(&config, &metrics, "abc", 106.001, 115.0).word, Some(0));
1136        // A hair before, and it does not.
1137        assert_eq!(at(&config, &metrics, "abc", 105.999, 115.0).word, None);
1138        // The second character's midpoint, 116, behaves the same way.
1139        assert_eq!(at(&config, &metrics, "abc", 116.0, 115.0).word, Some(0));
1140        assert_eq!(at(&config, &metrics, "abc", 116.001, 115.0).word, Some(1));
1141    }
1142
1143    /// Left of, right of, above and below the text. None of these fail, and
1144    /// each clamps to the nearest place rather than to nothing.
1145    #[test]
1146    fn points_outside_the_content_clamp_to_its_ends() {
1147        let config = config();
1148        let metrics = metrics();
1149        let layout = vt::layout("ABCDEFGH", &config, &metrics);
1150        let offset = centred(&layout, &config);
1151        let ask = |x: f32, y: f32| {
1152            place_at_point(
1153                &layout,
1154                config.plate,
1155                &config,
1156                &metrics,
1157                offset,
1158                Point::new(f64::from(x), f64::from(y)),
1159            )
1160        };
1161        // Far left of the first character, on the line.
1162        assert_eq!(ask(-1000.0, 115.0).word, None);
1163        // Far right of the last, on the line.
1164        assert_eq!(ask(1000.0, 115.0).word, Some(7));
1165        // Above everything: the layout's first place.
1166        assert_eq!(ask(134.0, 10_000.0), begin_place(&layout));
1167        // Below everything: its last.
1168        assert_eq!(ask(134.0, -10_000.0), end_place(&layout));
1169    }
1170
1171    /// An empty field has exactly one place, and every click finds it.
1172    #[test]
1173    fn an_empty_field_answers_its_only_place_from_anywhere() {
1174        let config = config();
1175        let metrics = metrics();
1176        let layout = vt::layout("", &config, &metrics);
1177        for (x, y) in [
1178            (102.0, 115.0),
1179            (198.0, 115.0),
1180            (134.0, 10_000.0),
1181            (134.0, -10_000.0),
1182            (-500.0, 115.0),
1183        ] {
1184            let place = place_at_point(
1185                &layout,
1186                config.plate,
1187                &config,
1188                &metrics,
1189                centred(&layout, &config),
1190                Point::new(x, y),
1191            );
1192            assert_eq!(place.word, None, "at ({x}, {y})");
1193            assert_eq!(word_index_of_place(&layout, place), 0);
1194        }
1195    }
1196
1197    /// A multi-line field: which line a y picks, and that an empty paragraph
1198    /// between two full ones is reachable.
1199    #[test]
1200    fn a_blank_line_between_paragraphs_is_its_own_place() {
1201        let config = Config {
1202            plate: geom::rect(0.0, 0.0, 200.0, 200.0),
1203            font_size: 10.0,
1204            multi_line: true,
1205            ..Config::default()
1206        };
1207        let metrics = tens();
1208        let layout = vt::layout("ab\n\ncd", &config, &metrics);
1209        assert_eq!(layout.sections.len(), 3, "three paragraphs");
1210        assert!(
1211            layout.sections.get(1).is_some_and(|s| s.words.is_empty()),
1212            "the middle one is empty"
1213        );
1214        // The empty paragraph's only place, reached through the index pair.
1215        let blank = place_of_word_index(&layout, 3);
1216        assert_eq!(blank, Place::new(1, 0, None));
1217        assert_eq!(word_index_of_place(&layout, blank), 3);
1218    }
1219
1220    /// A defaulted `Place` is the beginning of the text, not the position
1221    /// after the first character.
1222    #[test]
1223    fn a_default_place_is_the_start_and_not_a_zero_word() {
1224        assert_eq!(Place::default(), Place::START);
1225        assert_eq!(Place::default(), Place::start());
1226        assert_eq!(Place::START.word, None, "a zero word is after character 0");
1227    }
1228
1229    /// A wrapped line's header is the previous line's end, and it is a
1230    /// *caret position*, not zero.
1231    ///
1232    /// A header on any line but the first folds onto the place before it
1233    /// before the count begins. Without the fold a click at the start of the
1234    /// second visual line reports index 0 — the start of the whole field —
1235    /// and the next keystroke is inserted there.
1236    #[test]
1237    fn a_wrapped_lines_header_indexes_to_the_end_of_the_line_above() {
1238        // `tens()` is ten thousandths of an em, so at 10pt each character
1239        // advances 0.1 units: a 0.45-unit plate holds four per line and
1240        // "abcdefgh" wraps after "abcd".
1241        let config = Config {
1242            plate: geom::rect(0.0, 0.0, 0.45, 200.0),
1243            font_size: 10.0,
1244            multi_line: true,
1245            auto_return: true,
1246            ..Config::default()
1247        };
1248        let layout = vt::layout("abcdefgh", &config, &tens());
1249        let section = layout.sections.first().expect("one section");
1250        assert!(section.lines.len() >= 2, "the text must wrap: {section:?}");
1251        let first_line_end = section.lines.first().expect("a first line").last_word();
1252
1253        // The second line's header, which is what a click left of its first
1254        // midpoint produces.
1255        let header = Place::new(0, 1, None);
1256        assert_eq!(
1257            word_index_of_place(&layout, header),
1258            word_index_of_place(&layout, Place::new(0, 0, first_line_end)),
1259            "the header must be the line above's end, not the field's start"
1260        );
1261        assert_ne!(word_index_of_place(&layout, header), 0);
1262    }
1263
1264    /// A place naming a section the layout does not have is the end, and the
1265    /// end is not one past it.
1266    ///
1267    /// The section-break character is counted between sections and not after
1268    /// the last one, so the clamp adds no trailing break.
1269    #[test]
1270    fn a_place_past_the_last_section_indexes_to_the_very_end() {
1271        let config = Config {
1272            plate: geom::rect(0.0, 0.0, 200.0, 200.0),
1273            font_size: 10.0,
1274            multi_line: true,
1275            ..Config::default()
1276        };
1277        for text in ["abc", "ab\ncd", "a\nb\nc"] {
1278            let layout = vt::layout(text, &config, &tens());
1279            assert_eq!(
1280                word_index_of_place(&layout, Place::new(9, 0, Some(0))),
1281                word_index_of_place(&layout, end_place(&layout)),
1282                "{text:?}"
1283            );
1284        }
1285    }
1286
1287    /// Every place a layout can name survives being turned into an index and
1288    /// back.
1289    #[test]
1290    fn every_place_survives_the_index_round_trip() {
1291        let config = Config {
1292            plate: geom::rect(0.0, 0.0, 200.0, 200.0),
1293            font_size: 10.0,
1294            multi_line: true,
1295            ..Config::default()
1296        };
1297        let metrics = tens();
1298        for text in ["", "a", "abc", "ab\ncd", "ab\n\ncd", "a\nb\nc\nd"] {
1299            let layout = vt::layout(text, &config, &metrics);
1300            for (s, section) in layout.sections.iter().enumerate() {
1301                for (l, line) in section.lines.iter().enumerate() {
1302                    // The header, then every character the line covers.
1303                    let words = std::iter::once(None)
1304                        .chain(line.words.clone().into_iter().flatten().map(Some));
1305                    for word in words {
1306                        let place = Place::new(
1307                            u32::try_from(s).unwrap_or(0),
1308                            u32::try_from(l).unwrap_or(0),
1309                            word,
1310                        );
1311                        let index = word_index_of_place(&layout, place);
1312                        // A later line's header is not a caret position of
1313                        // its own: it collapses to the end of the line above,
1314                        // which is where the round trip lands. Asserting the
1315                        // collapse *target* is the point — skipping the case
1316                        // is what let it collapse to zero unnoticed.
1317                        let expected = if word.is_none() && l > 0 {
1318                            let previous = section.lines.get(l - 1).expect("a line above");
1319                            place_of_word_index(
1320                                &layout,
1321                                word_index_of_place(
1322                                    &layout,
1323                                    Place::new(
1324                                        u32::try_from(s).unwrap_or(0),
1325                                        u32::try_from(l - 1).unwrap_or(0),
1326                                        previous.last_word(),
1327                                    ),
1328                                ),
1329                            )
1330                        } else {
1331                            place
1332                        };
1333                        assert_eq!(
1334                            place_of_word_index(&layout, index),
1335                            expected,
1336                            "{text:?} at {place:?} (index {index})"
1337                        );
1338                    }
1339                }
1340            }
1341        }
1342    }
1343
1344    /// An index past the text's end is the last place, not a panic.
1345    #[test]
1346    fn an_index_past_the_end_answers_the_last_place() {
1347        let config = config();
1348        let metrics = metrics();
1349        let layout = vt::layout("ABC", &config, &metrics);
1350        assert_eq!(place_of_word_index(&layout, 3), end_place(&layout));
1351        assert_eq!(place_of_word_index(&layout, 9999), end_place(&layout));
1352    }
1353
1354    /// Clicking where a caret is drawn puts the caret back where it was:
1355    /// `place_at_point` and `point_at_place` agree at every position.
1356    #[test]
1357    fn a_caret_clicked_at_its_own_position_does_not_move() {
1358        let config = config();
1359        let metrics = metrics();
1360        let layout = vt::layout("ABCDEFGH", &config, &metrics);
1361        let offset = centred(&layout, &config);
1362        for word in carets(8) {
1363            let place = Place::new(0, 0, word);
1364            let point = point_at_place(&layout, config.plate, &config, &metrics, offset, place);
1365            // A click *at* the caret is exactly on a character boundary,
1366            // which is a half-width past the previous character's midpoint —
1367            // strictly past it, so the caret stays put.
1368            let back = place_at_point(
1369                &layout,
1370                config.plate,
1371                &config,
1372                &metrics,
1373                offset,
1374                Point::new(point.x, point.y - 1.0),
1375            );
1376            assert_eq!(back, place, "caret at {word:?} moved");
1377        }
1378    }
1379
1380    /// The caret rectangle a focused field draws: as wide as asked, as tall
1381    /// as its line, and starting where the character it follows ends.
1382    #[test]
1383    fn the_caret_rectangle_spans_its_line() {
1384        let config = config();
1385        let metrics = metrics();
1386        let layout = vt::layout("ABCDEFGH", &config, &metrics);
1387        let rect = caret_rect(
1388            &layout,
1389            config.plate,
1390            &config,
1391            &metrics,
1392            (0.0, 0.0),
1393            Place::new(0, 0, Some(3)),
1394            0.4,
1395        );
1396        // After D, whose advance ends at 134.336.
1397        assert!(
1398            (geom::left(rect) - 134.336).abs() < 0.01,
1399            "{:?}",
1400            geom::left(rect)
1401        );
1402        assert!((geom::width(rect) - 0.4).abs() < 1e-5);
1403        // As tall as Helvetica 12's ascent plus descent.
1404        let expected = (723.0 + 207.0) * 12.0 * 0.001;
1405        assert!(
1406            (geom::height(rect) - expected).abs() < 0.01,
1407            "{:?} vs {expected}",
1408            geom::height(rect)
1409        );
1410    }
1411
1412    /// A place naming something the layout does not have answers a valid
1413    /// point rather than panicking — the caret can outlive an edit that
1414    /// shortened the text.
1415    #[test]
1416    fn a_place_past_the_layout_still_answers_a_point() {
1417        let config = config();
1418        let metrics = metrics();
1419        let layout = vt::layout("AB", &config, &metrics);
1420        for place in [
1421            Place::new(9, 0, Some(0)),
1422            Place::new(0, 9, Some(0)),
1423            Place::new(0, 0, Some(99)),
1424            Place::new(u32::MAX, u32::MAX, Some(u32::MAX)),
1425        ] {
1426            let point = point_at_place(&layout, config.plate, &config, &metrics, (0.0, 0.0), place);
1427            assert!(point.x.is_finite() && point.y.is_finite(), "{place:?}");
1428            // The index of an out-of-range place is whatever the walk
1429            // accumulated before it ran out — a number, never a panic — and
1430            // feeding it back always lands on a place the layout does hold.
1431            let index = word_index_of_place(&layout, place);
1432            let back = place_of_word_index(&layout, index);
1433            assert!(
1434                (back.section as usize) < layout.sections.len(),
1435                "{place:?} came back as {back:?}"
1436            );
1437        }
1438    }
1439
1440    /// A comb field's cells: the tail each character carries widens its
1441    /// advance, so the midpoints are the cells' midpoints and not the
1442    /// glyphs'.
1443    #[test]
1444    fn comb_cells_hit_test_on_the_cell_rather_than_the_glyph() {
1445        let config = Config {
1446            plate: geom::rect(0.0, 0.0, 100.0, 20.0),
1447            font_size: 10.0,
1448            char_array: 4,
1449            ..Config::default()
1450        };
1451        let metrics = tens();
1452        let layout = vt::layout("ab", &config, &metrics);
1453        // Four cells across a hundred units: 25 each. The first character's
1454        // advance therefore runs a full cell, so its midpoint is far right of
1455        // the glyph's own centre.
1456        let first = layout
1457            .sections
1458            .first()
1459            .and_then(|s| s.words.first())
1460            .copied()
1461            .expect("a first character");
1462        let width = vt::word_width(&first, &config, &metrics, config.font_size);
1463        assert!(width > 20.0, "the cell tail widened the advance: {width}");
1464        let mid = first.x + width * 0.5;
1465        let before = place_at_point(
1466            &layout,
1467            config.plate,
1468            &config,
1469            &metrics,
1470            (0.0, 0.0),
1471            Point::new(f64::from(mid) - 0.01, 10.0),
1472        );
1473        let after = place_at_point(
1474            &layout,
1475            config.plate,
1476            &config,
1477            &metrics,
1478            (0.0, 0.0),
1479            Point::new(f64::from(mid) + 0.01, 10.0),
1480        );
1481        assert_eq!(before.word, None);
1482        assert_eq!(after.word, Some(0));
1483    }
1484
1485    /// Every character five hundred per mille, so a twelve-point run steps in
1486    /// whole sixes and the caret positions below are exact.
1487    fn half_em(_: u32) -> i32 {
1488        500
1489    }
1490
1491    fn halves() -> Metrics<'static> {
1492        Metrics {
1493            width: &half_em,
1494            ascent: 1000,
1495            descent: -200,
1496        }
1497    }
1498
1499    /// A three-character Hebrew word in a plate whose left edge is one, laid
1500    /// out at twelve points with six-unit advances.
1501    ///
1502    /// The run is right-to-left, so it is drawn right to left across
1503    /// `1 ..= 19`: the first character in logical order occupies `13 ..= 19`,
1504    /// the second `7 ..= 13`, the third `1 ..= 7`.
1505    fn rtl_run() -> (Config, Metrics<'static>, vt::Layout) {
1506        let config = Config {
1507            plate: geom::rect(1.0, 1.0, 99.0, 29.0),
1508            font_size: 12.0,
1509            ..Config::default()
1510        };
1511        let metrics = halves();
1512        let layout = vt::layout("\u{5D1}\u{5D7}\u{5E8}", &config, &metrics);
1513        (config, metrics, layout)
1514    }
1515
1516    /// The four caret positions of a right-to-left word, in place order.
1517    ///
1518    /// A character's caret sits at its own left edge when the run is
1519    /// right-to-left rather than at its trailing edge, and the line header
1520    /// sits at the line's *right* end. Together those walk the caret
1521    /// **leftward** as the place advances: the header at the run's right end,
1522    /// each further character one advance left of the last.
1523    ///
1524    /// Adding the advance in both directions instead — which is what a
1525    /// left-to-right-only port does — gives `1, 19, 13, 7`: the header at the
1526    /// wrong end, and every other caret one full advance right of the gap it
1527    /// names.
1528    #[test]
1529    fn a_right_to_left_words_carets_walk_leftward_from_its_right_edge() {
1530        let (config, metrics, layout) = rtl_run();
1531        let got: Vec<f64> = carets(3)
1532            .map(|word| {
1533                point_at_place(
1534                    &layout,
1535                    config.plate,
1536                    &config,
1537                    &metrics,
1538                    (0.0, 0.0),
1539                    Place::new(0, 0, word),
1540                )
1541                .x
1542            })
1543            .collect();
1544        let want = [19.0, 13.0, 7.0, 1.0];
1545        for (place, (&got, &want)) in got.iter().zip(want.iter()).enumerate() {
1546            assert!(
1547                (got - want).abs() < 1e-4,
1548                "place {} caret is {got}, want {want}: {got:?}",
1549                i32::try_from(place).unwrap_or(0) - 1
1550            );
1551        }
1552    }
1553
1554    /// The same four positions through [`caret_rect`], which is what the
1555    /// focused field actually draws, so the rectangle's left edge is the
1556    /// caret's x rather than that x plus a width.
1557    #[test]
1558    fn the_drawn_caret_rectangle_follows_the_same_right_to_left_walk() {
1559        let (config, metrics, layout) = rtl_run();
1560        for (word, want) in carets(3).zip([19.0_f64, 13.0, 7.0, 1.0]) {
1561            let rect = caret_rect(
1562                &layout,
1563                config.plate,
1564                &config,
1565                &metrics,
1566                (0.0, 0.0),
1567                Place::new(0, 0, word),
1568                0.4,
1569            );
1570            assert!(
1571                (rect.x0 - want).abs() < 1e-4,
1572                "caret after {word:?} starts at {}, want {want}",
1573                rect.x0
1574            );
1575            assert!((rect.x1 - rect.x0 - 0.4).abs() < 1e-4, "{rect:?}");
1576        }
1577    }
1578
1579    /// A left-to-right line is untouched: its header is the left edge and
1580    /// every caret is a character's right edge, which is what every existing
1581    /// assertion in this module already depends on.
1582    #[test]
1583    fn a_left_to_right_run_still_walks_rightward_from_its_left_edge() {
1584        let config = Config {
1585            plate: geom::rect(1.0, 1.0, 99.0, 29.0),
1586            font_size: 12.0,
1587            ..Config::default()
1588        };
1589        let metrics = halves();
1590        let layout = vt::layout("abc", &config, &metrics);
1591        let got: Vec<f64> = carets(3)
1592            .map(|word| {
1593                point_at_place(
1594                    &layout,
1595                    config.plate,
1596                    &config,
1597                    &metrics,
1598                    (0.0, 0.0),
1599                    Place::new(0, 0, word),
1600                )
1601                .x
1602            })
1603            .collect();
1604        for (place, (&got, &want)) in got.iter().zip([1.0, 7.0, 13.0, 19.0].iter()).enumerate() {
1605            assert!(
1606                (got - want).abs() < 1e-4,
1607                "place {} caret is {got}, want {want}",
1608                i32::try_from(place).unwrap_or(0) - 1
1609            );
1610        }
1611    }
1612
1613    /// A click in a right-to-left run answers one of the run's two ends, and
1614    /// the **middle** character's midpoint is what divides them.
1615    ///
1616    /// The word search is a **bisection**, and the midpoint predicate it
1617    /// bisects on is monotone only along a left-to-right line. Down a
1618    /// descending run the first probe is the middle character and the answer
1619    /// follows that one probe alone: a click **left** of its midpoint narrows
1620    /// to index 0, whose own midpoint is the run's *rightmost*, so the
1621    /// post-loop test there fails and the answer is the header; a click right
1622    /// of it narrows to the last index, whose midpoint is the run's
1623    /// *leftmost*, which the click is past.
1624    ///
1625    /// The two places are the run's two logical ends — its beginning and its
1626    /// end — and in a right-to-left run those are drawn at the right and left
1627    /// edges respectively. So the caret crosses the whole run at the middle
1628    /// character's midpoint rather than stepping character by character. The
1629    /// assertion here is that transcription, not a claim that it is the
1630    /// nicest answer. What a scan-based search does instead is worse and not
1631    /// merely different — it answers the header for **every** click in the
1632    /// run, so the caret never moves at all.
1633    #[test]
1634    fn a_click_in_a_right_to_left_run_answers_the_end_the_bisection_narrows_to() {
1635        let (config, metrics, layout) = rtl_run();
1636        let at = |x: f64| {
1637            place_at_point(
1638                &layout,
1639                config.plate,
1640                &config,
1641                &metrics,
1642                (0.0, 0.0),
1643                Point::new(x, 15.0),
1644            )
1645            .word
1646        };
1647        let caret = |x: f64| {
1648            point_at_place(
1649                &layout,
1650                config.plate,
1651                &config,
1652                &metrics,
1653                (0.0, 0.0),
1654                Place::new(0, 0, at(x)),
1655            )
1656            .x
1657        };
1658        // The run occupies PDF x 1..19. The middle character is drawn over
1659        // 7..13, so its midpoint is at 10 — the one probe that decides.
1660        assert_eq!(at(2.0), None, "left of the middle midpoint: the header");
1661        assert_eq!(at(9.9), None);
1662        assert_eq!(at(10.0), None, "exactly on the midpoint is not past it");
1663        assert_eq!(at(10.1), Some(2), "past it: the run's last character");
1664        assert_eq!(at(18.0), Some(2));
1665
1666        // The two places are the run's logical ends, drawn at its right and
1667        // left edges: the caret crosses the run rather than stepping across
1668        // it.
1669        assert!((caret(2.0) - 19.0).abs() < 1e-4, "{}", caret(2.0));
1670        assert!((caret(18.0) - 1.0).abs() < 1e-4, "{}", caret(18.0));
1671    }
1672
1673    /// A line whose first character is right-to-left answers its right edge
1674    /// for the header even when a left-to-right run follows it, because
1675    /// `GetLineCaretX` reads the **first word alone**.
1676    #[test]
1677    fn a_mixed_line_takes_its_header_from_its_first_word_only() {
1678        let config = Config {
1679            plate: geom::rect(1.0, 1.0, 99.0, 29.0),
1680            font_size: 12.0,
1681            ..Config::default()
1682        };
1683        let metrics = halves();
1684        let rtl_first = vt::layout("\u{5D1}\u{5D7}ab", &config, &metrics);
1685        let header = point_at_place(
1686            &rtl_first,
1687            config.plate,
1688            &config,
1689            &metrics,
1690            (0.0, 0.0),
1691            Place::START,
1692        )
1693        .x;
1694        let line_width = rtl_first
1695            .sections
1696            .first()
1697            .and_then(|section| section.lines.first())
1698            .map_or(0.0, |line| line.width);
1699        assert!(
1700            (header - f64::from(1.0 + line_width)).abs() < 1e-4,
1701            "header is the line's right edge: {header} against width {line_width}"
1702        );
1703
1704        // And the mirror: a left-to-right first word keeps the left edge even
1705        // with a right-to-left run behind it.
1706        let ltr_first = vt::layout("ab\u{5D1}\u{5D7}", &config, &metrics);
1707        let header = point_at_place(
1708            &ltr_first,
1709            config.plate,
1710            &config,
1711            &metrics,
1712            (0.0, 0.0),
1713            Place::START,
1714        )
1715        .x;
1716        assert!((header - 1.0).abs() < 1e-4, "{header}");
1717    }
1718
1719    /// An empty line has no first character to take a direction from, so its
1720    /// header stays at the line's own left edge.
1721    #[test]
1722    fn an_empty_line_keeps_its_left_edge_for_a_header() {
1723        let config = Config {
1724            plate: geom::rect(1.0, 1.0, 99.0, 29.0),
1725            font_size: 12.0,
1726            ..Config::default()
1727        };
1728        let metrics = halves();
1729        let layout = vt::layout("", &config, &metrics);
1730        let header = point_at_place(
1731            &layout,
1732            config.plate,
1733            &config,
1734            &metrics,
1735            (0.0, 0.0),
1736            Place::START,
1737        )
1738        .x;
1739        assert!((header - 1.0).abs() < 1e-4, "{header}");
1740    }
1741
1742    /// The `password` fixture's own geometry, laid out with the size it asks
1743    /// for — **zero**, meaning automatic.
1744    ///
1745    /// `testing/resources/pixel/password.in`'s second widget is
1746    /// `/Rect [100 160 200 190]` with `/MaxLen 5`, `/Q 2` and the password
1747    /// flag, and the file carries no `/DA` and no `/DR` at all — so the size
1748    /// requested is zero and the fallback Helvetica substitutes to Arimo
1749    /// (ascent 905, descent −211, `'*'` 389/1000). The evt types `tiger` and
1750    /// four more characters the limit drops.
1751    fn password_field() -> (Config, Metrics<'static>, vt::Layout) {
1752        fn arimo(ch: u32) -> i32 {
1753            match char::from_u32(ch) {
1754                Some('*') => 389,
1755                Some('t') => 278,
1756                Some('i') => 222,
1757                Some('r') => 333,
1758                _ => 556,
1759            }
1760        }
1761        let config = Config {
1762            // `/Rect` deflated by the default one-unit border.
1763            plate: geom::rect(101.0, 161.0, 199.0, 189.0),
1764            alignment: vt::Alignment::Right,
1765            font_size: 0.0,
1766            sub_word: Some('*'),
1767            limit_char: 5,
1768            ..Config::default()
1769        };
1770        let metrics = Metrics {
1771            width: &arimo,
1772            ascent: 905,
1773            descent: -211,
1774        };
1775        let layout = vt::layout("tigerssss", &config, &metrics);
1776        (config, metrics, layout)
1777    }
1778
1779    /// An auto-sized field's carets are measured at the size the layout
1780    /// resolved, not at the zero its config still holds.
1781    ///
1782    /// This is `password`'s defect, in one assertion. `vt::layout` resolves
1783    /// the zero to 25 and writes it to [`vt::Layout::font_size`] — which is
1784    /// what `CPVT_VariableText::Rearrange` does with
1785    /// `SetFontSize(GetAutoFontSize())` — and `edit_ap` already draws with
1786    /// that. Reading `config.font_size` here instead made every advance zero,
1787    /// so the caret after the last of five asterisks sat on that asterisk's
1788    /// **left** edge: 189.275 rather than 199.0, which is the ten device
1789    /// columns the golden showed.
1790    ///
1791    /// The five carets are one advance apart and the last is the plate's own
1792    /// right edge, because the field is right-aligned and full.
1793    #[test]
1794    fn an_auto_sized_fields_carets_are_measured_at_the_size_the_layout_resolved() {
1795        let (config, metrics, layout) = password_field();
1796        assert!(
1797            (layout.font_size - 25.0).abs() < 1e-4,
1798            "auto size resolved to {}",
1799            layout.font_size
1800        );
1801        assert!(
1802            config.font_size == 0.0,
1803            "the config still holds the request, which is the whole point"
1804        );
1805        // The limit keeps five characters of the nine typed.
1806        assert_eq!(end_place(&layout), Place::new(0, 0, Some(4)));
1807
1808        let advance = 389.0 * 25.0 / 1000.0;
1809        let first = 150.375_f32;
1810        for word in carets(5) {
1811            let got = point_at_place(
1812                &layout,
1813                config.plate,
1814                &config,
1815                &metrics,
1816                (0.0, 0.0),
1817                Place::new(0, 0, word),
1818            )
1819            .x;
1820            // The header sits at the line's left edge; character `w` sits
1821            // one advance further along than character `w - 1`.
1822            let steps = u16::try_from(word.map_or(0, |w| w.saturating_add(1)))
1823                .map_or(f32::from(u16::MAX), f32::from);
1824            let want = f64::from(first + advance * steps);
1825            assert!(
1826                (got - want).abs() < 1e-3,
1827                "caret after {word:?} is {got}, want {want}"
1828            );
1829        }
1830    }
1831
1832    /// The same size, through [`caret_rect`], which is what the focused field
1833    /// draws — the rectangle's x **and** its height.
1834    ///
1835    /// The height is the second half of the same defect: `line_extent`'s
1836    /// fallback read the config too, so a place naming a line the layout does
1837    /// not have would have answered a caret of no height at all.
1838    #[test]
1839    fn the_drawn_caret_of_an_auto_sized_field_has_the_resolved_sizes_height() {
1840        let (config, metrics, layout) = password_field();
1841        let rect = caret_rect(
1842            &layout,
1843            config.plate,
1844            &config,
1845            &metrics,
1846            (0.0, 0.0),
1847            end_place(&layout),
1848            0.4,
1849        );
1850        // Flush with the plate's right edge: right-aligned and full.
1851        assert!((rect.x0 - 199.0).abs() < 1e-3, "{rect:?}");
1852        assert!((rect.x1 - rect.x0 - 0.4).abs() < 1e-4, "{rect:?}");
1853        // (905 + 211) * 25 / 1000, the line's own ascent and descent.
1854        assert!((rect.y1 - rect.y0 - 27.9).abs() < 1e-3, "{rect:?}");
1855
1856        // A place naming a line the layout does **not** have keeps the
1857        // config's size, which for an auto-sized field is a caret of no
1858        // height. That is deliberate and measured — see `line_extent` — and
1859        // it is asserted here so substituting the layout's size turns this
1860        // red rather than moving 170 conformance rows silently.
1861        let missing = caret_rect(
1862            &layout,
1863            config.plate,
1864            &config,
1865            &metrics,
1866            (0.0, 0.0),
1867            Place::new(0, 7, Some(0)),
1868            0.4,
1869        );
1870        assert!((missing.y1 - missing.y0).abs() < 1e-6, "{missing:?}");
1871    }
1872
1873    /// A click into an auto-sized field lands by the midpoint rule at the
1874    /// resolved size, not by position alone.
1875    ///
1876    /// The third face of the same defect. At a zero size every advance is
1877    /// zero, so every midpoint collapses onto the character's own origin and
1878    /// the bisection answers whichever end its first probe falls on — the
1879    /// caret would jump to a line end instead of stepping.
1880    #[test]
1881    fn a_click_in_an_auto_sized_field_uses_the_resolved_midpoints() {
1882        let (config, metrics, layout) = password_field();
1883        let advance = 389.0 * 25.0 / 1000.0;
1884        let first = f64::from(150.375_f32);
1885        for index in 0..5 {
1886            // A hair past the midpoint of character `index` lands after it.
1887            let x = first + advance * (f64::from(index) + 0.5) + 0.01;
1888            let place = place_at_point(
1889                &layout,
1890                config.plate,
1891                &config,
1892                &metrics,
1893                (0.0, 0.0),
1894                Point::new(x, 175.0),
1895            );
1896            assert_eq!(place.word, Some(index), "click at {x} landed at {place:?}");
1897        }
1898        // Left of the first character's midpoint is the line header.
1899        let header = place_at_point(
1900            &layout,
1901            config.plate,
1902            &config,
1903            &metrics,
1904            (0.0, 0.0),
1905            Point::new(first + advance * 0.5 - 0.01, 175.0),
1906        );
1907        assert_eq!(header.word, None, "{header:?}");
1908    }
1909
1910    /// A field that asks for an explicit size is untouched, which is why
1911    /// every other assertion in this module still reads the same.
1912    ///
1913    /// The two sizes are the same number there, so the fix can only move an
1914    /// auto-sized field — and an auto-sized field is exactly the one no
1915    /// assertion covered.
1916    #[test]
1917    fn an_explicitly_sized_field_answers_the_same_as_it_always_did() {
1918        let config = config();
1919        let metrics = metrics();
1920        let layout = vt::layout("ABC", &config, &metrics);
1921        assert!((layout.font_size - config.font_size).abs() < f32::EPSILON);
1922        let caret = point_at_place(
1923            &layout,
1924            config.plate,
1925            &config,
1926            &metrics,
1927            (0.0, 0.0),
1928            Place::new(0, 0, Some(0)),
1929        )
1930        .x;
1931        // 'A' is 667/1000 at twelve points, from the plate's left edge.
1932        assert!(
1933            (caret - (101.0 + 667.0 * 12.0 / 1000.0)).abs() < 1e-3,
1934            "{caret}"
1935        );
1936    }
1937}