Skip to main content

rhythm_gpui/
metrics.rs

1//! Placement metrics for shaped lines and blocks — the value-only layer for
2//! custom renderers that paint cached shaped lines directly instead of
3//! composing elements.
4//!
5//! [`FontRhythm`] describes a text *style*; [`RhythmLineMetrics`] describes
6//! one *actual shaped line*, whose `ascent`/`descent` come from the shaping
7//! result (in gpui, the maximum over the line's explicit font runs). A line
8//! mixing bold, italic, inline code, or explicit CJK/emoji fonts keeps one
9//! baseline placed from those maxima, so only shaped metrics can land it on
10//! the grid. [`RhythmBlockMetrics`] combines a line with baseline or ink-top
11//! anchors into whole-row block geometry, with the exact row arithmetic a
12//! virtualized renderer needs.
13//!
14//! Everything here is a small `Copy` value. Construction and scalar geometry
15//! methods are O(1); [`RhythmLineMetrics::covering`] is O(`metrics.len()`). All
16//! of them are allocation-free and lock-free, and nothing touches a text
17//! system.
18
19use crate::Rhythm;
20
21/// Vertical metrics of one shaped line on the rhythm grid.
22///
23/// Unlike [`FontRhythm`](crate::FontRhythm), which carries a resolved *style*
24/// (with its font size and optional cap/x heights), this is the minimal value
25/// a renderer needs to place one already-shaped line: the line's reported
26/// `ascent`/`descent`, its height in whole rhythm units, and the grid. In
27/// gpui, feed it `WrappedLine::ascent()` / `descent()` — the shaped maxima
28/// over the line's explicit font runs — and every visual line of that
29/// `WrappedLine` lands on the grid, because wrapped lines advance by the same
30/// whole-row line height.
31///
32/// The renderer chooses `line_rhythms`; [`min_line_rhythms`](Self::min_line_rhythms)
33/// suggests the smallest count whose line box contains that metric envelope, and
34/// [`overflows_line_box`](Self::overflows_line_box) reports when the chosen
35/// count is smaller. Keeping a smaller count is valid — the baseline stays on
36/// the grid and the reported envelope overflows symmetrically via negative
37/// half-leading, exactly as CSS line boxes behave. This does not claim that
38/// every glyph's typographic or raster ink stays inside the line box.
39/// [`covering`](Self::covering) picks the count the other way round: over a
40/// known set of faces, before anything is shaped.
41///
42/// A shaped *empty* line has zero ascent and descent; place empty lines with
43/// the style's [`FontRhythm`](crate::FontRhythm) metrics instead so blank
44/// lines keep the style's baseline position.
45///
46/// # Examples
47///
48/// ```
49/// use rhythm_gpui::{Rhythm, RhythmLineMetrics};
50///
51/// let grid = Rhythm::new(8.0);
52/// // A shaped line whose tallest run gives ascent 15.2, descent 4.1.
53/// let line = RhythmLineMetrics::new(15.2, 4.1, 3, grid);
54///
55/// // Land its baseline on the 5th grid line below the block top: the paint
56/// // origin is where the line box's top edge goes.
57/// let origin_y = line.paint_origin_for(grid.height(5));
58/// assert!((origin_y + line.baseline_above() - grid.height(5)).abs() < 1e-4);
59/// ```
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct RhythmLineMetrics {
62    ascent: f32,
63    descent: f32,
64    line_rhythms: u32,
65    grid: Rhythm,
66}
67
68impl RhythmLineMetrics {
69    /// Build from a shaped line's metrics: `ascent` and `descent` in logical
70    /// pixels (both non-negative, e.g. gpui's `WrappedLine::ascent()` /
71    /// `descent()`), and the line height in whole rhythm units.
72    ///
73    /// # Panics
74    ///
75    /// Panics when `ascent` or `descent` is negative or non-finite, or when
76    /// `line_rhythms` is zero.
77    #[inline]
78    pub fn new(ascent: f32, descent: f32, line_rhythms: u32, grid: Rhythm) -> Self {
79        assert!(
80            ascent.is_finite() && ascent >= 0.0,
81            "ascent must be finite and non-negative"
82        );
83        assert!(
84            descent.is_finite() && descent >= 0.0,
85            "descent must be finite and non-negative"
86        );
87        assert!(line_rhythms > 0, "line_rhythms must be greater than zero");
88        Self {
89            ascent,
90            descent,
91            line_rhythms,
92            grid,
93        }
94    }
95
96    /// Like [`new`](Self::new), but grows the line box to
97    /// [`min_line_rhythms`](Self::min_line_rhythms) when `line_rhythms` is too
98    /// small to contain the reported ascent/descent envelope. Use it when the
99    /// configured line height is a floor rather than a fixed virtualization
100    /// budget.
101    ///
102    /// # Panics
103    ///
104    /// Panics under the same conditions as [`new`](Self::new), or when the
105    /// minimum fitting count does not fit in `u32`.
106    pub fn at_least(ascent: f32, descent: f32, line_rhythms: u32, grid: Rhythm) -> Self {
107        let line = Self::new(ascent, descent, line_rhythms, grid);
108        Self {
109            line_rhythms: line_rhythms.max(line.min_line_rhythms()),
110            ..line
111        }
112    }
113
114    /// The smallest line box on `grid` containing every line in `metrics`:
115    /// the maximum ascent and the maximum descent over the set, in the
116    /// largest of their line heights, grown to
117    /// [`min_line_rhythms`](Self::min_line_rhythms) when that combined metric
118    /// envelope no longer fits.
119    ///
120    /// A line shapes to the maxima over its explicit font runs, so no line
121    /// drawn from a caller-supplied set of face metrics can exceed the box
122    /// covering that set. Folding a style's whole known set — its own face and
123    /// the faces its runs explicitly use (bold, inline code, or an explicit CJK
124    /// or emoji face) — at catalog-build time makes the line's metric envelope
125    /// a property of construction rather than one each shaped line has to be
126    /// checked for. Glyph-level fallback faces selected later by a platform
127    /// shaper are outside this set unless the caller supplies their metrics.
128    /// Nothing here shapes text: the count is known at startup, so a block's
129    /// height follows from its line count alone, which is what a virtualized
130    /// renderer needs.
131    ///
132    /// Take the count, not the box, into placement: this value's
133    /// `ascent`/`descent` describe a hypothetical line reaching both maxima
134    /// at once, so keep building each line's metrics from its own shaped
135    /// values — at this [`line_rhythms`](Self::line_rhythms) — and every
136    /// baseline still lands on the grid.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use rhythm_gpui::{Rhythm, RhythmLineMetrics};
142    ///
143    /// let grid = Rhythm::new(8.0);
144    /// // A body style, and a display face its lines can mix in.
145    /// let body = RhythmLineMetrics::new(14.67, 3.51, 3, grid);
146    /// let display = RhythmLineMetrics::new(28.8, 6.4, 3, grid);
147    ///
148    /// // One static row budget for the style: three rows cannot hold the
149    /// // display face's ascent/descent envelope, so the covering box is five.
150    /// let budget = RhythmLineMetrics::covering(&[body, display], grid);
151    /// assert_eq!(budget.line_rhythms(), 5);
152    ///
153    /// // Every mixture of the two fits it, including the worst case.
154    /// let worst = RhythmLineMetrics::new(28.8, 6.4, budget.line_rhythms(), grid);
155    /// assert!(!worst.overflows_line_box());
156    /// ```
157    ///
158    /// # Panics
159    ///
160    /// Panics when `metrics` is empty, when an entry was built on a
161    /// different grid, or when the minimum fitting count does not fit in
162    /// `u32`.
163    pub fn covering(metrics: &[RhythmLineMetrics], grid: Rhythm) -> Self {
164        assert!(
165            !metrics.is_empty(),
166            "a covering line box must cover at least one line"
167        );
168        let mut ascent = 0.0;
169        let mut descent = 0.0;
170        let mut line_rhythms = 1;
171        for line in metrics {
172            assert_eq!(
173                line.grid, grid,
174                "every covered line must be built on the covering grid"
175            );
176            ascent = f32::max(ascent, line.ascent);
177            descent = f32::max(descent, line.descent);
178            line_rhythms = line_rhythms.max(line.line_rhythms);
179        }
180        Self::at_least(ascent, descent, line_rhythms, grid)
181    }
182
183    /// The reported shaped ascent above the baseline, non-negative.
184    #[inline]
185    pub const fn ascent(&self) -> f32 {
186        self.ascent
187    }
188
189    /// The reported shaped descent below the baseline, non-negative.
190    #[inline]
191    pub const fn descent(&self) -> f32 {
192        self.descent
193    }
194
195    /// Line height in whole rhythm units.
196    #[inline]
197    pub const fn line_rhythms(&self) -> u32 {
198        self.line_rhythms
199    }
200
201    /// The grid the line is placed on.
202    #[inline]
203    pub const fn grid(&self) -> Rhythm {
204        self.grid
205    }
206
207    /// The line height in logical pixels: `line_rhythms × grid size`.
208    #[inline]
209    pub fn line_height(&self) -> f32 {
210        self.grid.size() * self.line_rhythms as f32
211    }
212
213    /// Extra space split above and below the `ascent + descent` box; negative
214    /// when that metric envelope is taller than the line box.
215    #[inline]
216    pub fn half_leading(&self) -> f32 {
217        (self.line_height() - self.ascent - self.descent) / 2.0
218    }
219
220    /// Distance from the line box's top edge down to the baseline, as gpui
221    /// paints it: `half_leading + ascent`.
222    #[inline]
223    pub fn baseline_above(&self) -> f32 {
224        self.half_leading() + self.ascent
225    }
226
227    /// Distance from the baseline down to the line box's bottom edge.
228    #[inline]
229    pub fn baseline_below(&self) -> f32 {
230        self.half_leading() + self.descent
231    }
232
233    /// Where to place the line box's top edge — the `origin.y` passed to
234    /// gpui's `WrappedLine::paint` — so the baseline lands exactly on
235    /// `target_baseline` (both in the same coordinate space):
236    /// `target_baseline − baseline_above`.
237    #[inline]
238    pub fn paint_origin_for(&self, target_baseline: f32) -> f32 {
239        target_baseline - self.baseline_above()
240    }
241
242    /// The smallest `line_rhythms` whose line box contains the reported
243    /// `ascent + descent` envelope, at least 1. Applies the same snapping rule
244    /// as [`Rhythm::snap_up`] — at `f64` precision, since shaped metrics are
245    /// summed rather than measured — so an envelope within a few rounding
246    /// steps of a whole-row height does not claim an extra row.
247    ///
248    /// Advisory: the renderer decides whether to grow the line or keep its
249    /// chosen height and accept the overflow.
250    ///
251    /// # Panics
252    ///
253    /// Panics when the minimum fitting count does not fit in `u32`.
254    #[inline]
255    pub fn min_line_rhythms(&self) -> u32 {
256        let rows = self.minimum_line_rows();
257        assert!(
258            rows <= f64::from(u32::MAX),
259            "minimum line rhythm count exceeds u32"
260        );
261        rows as u32
262    }
263
264    /// Whether the reported `ascent + descent` envelope is taller than the
265    /// chosen line box — equivalently, whether
266    /// [`half_leading`](Self::half_leading) is negative beyond
267    /// [`Rhythm::snap_up`]'s tolerance.
268    #[inline]
269    pub fn overflows_line_box(&self) -> bool {
270        self.minimum_line_rows() > f64::from(self.line_rhythms)
271    }
272
273    // The same snapping rule as `Rhythm::snap_rows`, computed in `f64`: the
274    // inputs are exact `f32` promotions, so here the division noise sits far
275    // below the tolerance instead of being what it absorbs. The duplication
276    // is deliberate — see the note there before merging them.
277    #[inline]
278    fn minimum_line_rows(&self) -> f64 {
279        let envelope = f64::from(self.ascent) + f64::from(self.descent);
280        let grid_size = f64::from(self.grid.size());
281        let rows = envelope / grid_size;
282        let nearest = rows.round();
283        let nearest_height = grid_size * nearest;
284        let tolerance = envelope * f64::from(f32::EPSILON) * 8.0;
285        let snapped = if (envelope - nearest_height).abs() <= tolerance {
286            nearest
287        } else {
288            rows.ceil()
289        };
290        snapped.max(1.0)
291    }
292}
293
294/// A text block on the rhythm grid as pure geometry: one line's metrics plus
295/// baseline or ink-top anchors, with both fragment geometry and the exact row
296/// arithmetic a virtualized renderer needs. [`new`](Self::new) is the pure
297/// form of `rhythm_block`; [`ink_anchored`](Self::ink_anchored) pairs an ink
298/// opening with its whole-row close.
299/// Either mode spans a whole number of rhythm rows for any number of lines, so
300/// blocks and fragments compose without breaking the page rhythm.
301///
302/// # Fragments
303///
304/// A block split at visual-line boundaries keeps its rhythm phase because
305/// every line advances by whole rows. The `first` / `middle` / `last` height
306/// methods expose concrete fragment geometry, while [`first_rows`](Self::first_rows),
307/// [`middle_rows`](Self::middle_rows), and [`last_rows`](Self::last_rows) expose
308/// exact `i32` cursor deltas. A virtualizer can accumulate those deltas in an
309/// `i64`, rebase near the viewport, then derive only a visible fragment's
310/// baseline with [`baseline_at_row`](Self::baseline_at_row) and line-box top
311/// with [`RhythmLineMetrics::paint_origin_for`].
312///
313/// # Examples
314///
315/// ```
316/// use rhythm_gpui::{Rhythm, RhythmBlockMetrics, RhythmLineMetrics};
317///
318/// let grid = Rhythm::new(8.0);
319/// // Georgia-like 16px body on a 3-unit (24px) line.
320/// let line = RhythmLineMetrics::new(14.67, 3.51, 3, grid);
321/// let block = RhythmBlockMetrics::new(line, 3, 1);
322///
323/// // Five wrapped lines cover sixteen whole rhythm rows…
324/// assert_eq!(block.rows(5), 3 + 1 + 4 * 3);
325///
326/// // …and the first baseline sits `top` grid lines below the block top.
327/// assert_eq!(block.first_baseline(), grid.height(3));
328/// let origin_y = line.paint_origin_for(block.first_baseline());
329/// assert!((origin_y + line.baseline_above() - grid.height(3)).abs() < 1e-4);
330/// ```
331#[derive(Debug, Clone, Copy, PartialEq)]
332pub struct RhythmBlockMetrics {
333    line: RhythmLineMetrics,
334    top: i32,
335    bottom: i32,
336    ink_ascent: Option<f32>,
337}
338
339impl RhythmBlockMetrics {
340    /// A block opening `top` rhythm units above the first baseline and
341    /// closing `bottom` units below the last. Negative counts are meaningful
342    /// for margin-style layouts, matching `baseline_top` / `baseline_bottom`.
343    pub const fn new(line: RhythmLineMetrics, top: i32, bottom: i32) -> Self {
344        Self {
345            line,
346            top,
347            bottom,
348            ink_ascent: None,
349        }
350    }
351
352    /// An ink-anchored block: opens `top` rhythm units above the anchored ink
353    /// top and closes with the paired trimmed space, retaining that ink phase
354    /// across fragments while the whole block still spans integer rows.
355    ///
356    /// `ink_ascent` is the anchored ink's height above the baseline: pass a
357    /// Latin cap height, or a caller-supplied CJK ICF ascent to anchor the
358    /// ideographic character face instead.
359    ///
360    /// # Panics
361    ///
362    /// Panics when `ink_ascent` is zero, negative, or non-finite.
363    pub fn ink_anchored(line: RhythmLineMetrics, ink_ascent: f32, top: i32, bottom: i32) -> Self {
364        assert!(
365            ink_ascent.is_finite() && ink_ascent > 0.0,
366            "ink anchor ascent must be finite and greater than zero"
367        );
368        Self {
369            line,
370            top,
371            bottom,
372            ink_ascent: Some(ink_ascent),
373        }
374    }
375
376    /// The line metrics the block is set in.
377    #[inline]
378    pub const fn line(&self) -> RhythmLineMetrics {
379        self.line
380    }
381
382    /// Opening rhythm units above the anchor (baseline or ink top). A row
383    /// count, not a length — [`opening`](Self::opening) is the pixel spacing
384    /// it produces.
385    #[inline]
386    pub const fn top_rhythms(&self) -> i32 {
387        self.top
388    }
389
390    /// Closing rhythm units below the last baseline, or the paired ink close.
391    /// A row count, not a length; see [`closing`](Self::closing).
392    #[inline]
393    pub const fn bottom_rhythms(&self) -> i32 {
394        self.bottom
395    }
396
397    fn grid(&self) -> Rhythm {
398        self.line.grid()
399    }
400
401    /// Space from the block's top edge down to its first line box. Negative
402    /// values are meaningful as margins rather than padding.
403    #[inline]
404    pub fn opening(&self) -> f32 {
405        let above = self.line.baseline_above();
406        match self.ink_ascent {
407            None => self.grid().height(self.top) - above,
408            Some(ink_ascent) => self.grid().height(self.top) - (above - ink_ascent),
409        }
410    }
411
412    /// Space from the last line box down to the block's whole-row bottom edge.
413    #[inline]
414    pub fn closing(&self) -> f32 {
415        match self.ink_ascent {
416            None => self.grid().height(self.bottom) - self.line.baseline_below(),
417            Some(ink_ascent) => {
418                self.grid().height(self.bottom) + (self.line.baseline_above() - ink_ascent)
419            }
420        }
421    }
422
423    /// Distance from the block's top edge down to the first baseline: `top`
424    /// whole rhythm units for baseline anchors, plus the ink ascent for
425    /// ink anchors. Applies to first and single fragments; in a middle/last
426    /// fragment the first baseline sits
427    /// [`RhythmLineMetrics::baseline_above`] below the fragment top.
428    #[inline]
429    pub fn first_baseline(&self) -> f32 {
430        self.baseline_at_row(i64::from(self.top))
431    }
432
433    /// Height of the whole block containing `lines` visual lines.
434    ///
435    /// # Panics
436    ///
437    /// Panics when `lines` is zero.
438    #[inline]
439    pub fn height(&self, lines: u32) -> f32 {
440        self.first_height(lines) + self.closing()
441    }
442
443    /// Height of a first fragment: opening plus `lines` whole line boxes.
444    ///
445    /// # Panics
446    ///
447    /// Panics when `lines` is zero.
448    #[inline]
449    pub fn first_height(&self, lines: u32) -> f32 {
450        self.opening() + self.middle_height(lines)
451    }
452
453    /// Height of a middle fragment containing `lines` whole line boxes.
454    ///
455    /// # Panics
456    ///
457    /// Panics when `lines` is zero.
458    #[inline]
459    pub fn middle_height(&self, lines: u32) -> f32 {
460        assert!(lines > 0, "a fragment must contain at least one line");
461        self.line.line_height() * lines as f32
462    }
463
464    /// Height of a last fragment: `lines` whole line boxes plus the closing.
465    ///
466    /// # Panics
467    ///
468    /// Panics when `lines` is zero.
469    #[inline]
470    pub fn last_height(&self, lines: u32) -> f32 {
471        self.middle_height(lines) + self.closing()
472    }
473
474    /// The whole number of rhythm rows a single-fragment block of `lines`
475    /// visual lines covers, as exact integer arithmetic:
476    /// `top + bottom + (lines − 1) × line_rhythms` for baseline anchors, or
477    /// `top + bottom + lines × line_rhythms` for ink anchors. The block's
478    /// height in pixels is `grid.height(rows(lines))`.
479    ///
480    /// # Panics
481    ///
482    /// Panics when `lines` is zero or the resulting row count does not fit in
483    /// `i32`.
484    #[inline]
485    pub fn rows(&self, lines: u32) -> i32 {
486        checked_rows(
487            i128::from(self.top) + self.spanned_rows(lines) + i128::from(self.bottom)
488                - self.trailing_rows(),
489        )
490    }
491
492    /// Row-cursor delta from the block top to the baseline immediately after a
493    /// *first* fragment of `lines` visual lines. Add it to the block's starting
494    /// row, then pass the result to [`baseline_at_row`](Self::baseline_at_row)
495    /// to place the first line of the next fragment.
496    ///
497    /// This and its two siblings partition [`rows`](Self::rows) exactly:
498    /// `first_rows(a) + middle_rows(b) + last_rows(c) == rows(a + b + c)`.
499    /// These values are cursor transitions, not fragment heights converted to
500    /// rows: continuation fragments start at a line-box top, generally between
501    /// grid lines. Keeping the cursor as rows and deriving only the visible
502    /// fragment's baseline prevents accumulated floating-point drift.
503    ///
504    /// # Examples
505    ///
506    /// ```
507    /// use rhythm_gpui::{Rhythm, RhythmBlockMetrics, RhythmLineMetrics};
508    ///
509    /// let grid = Rhythm::new(8.0);
510    /// let line = RhythmLineMetrics::new(14.67, 3.51, 3, grid);
511    /// let block = RhythmBlockMetrics::new(line, 3, 1);
512    ///
513    /// // A nine-line block split 2 / 4 / 3 across three viewport pages: the
514    /// // middle fragment starts two line advances below the first line box.
515    /// let first_origin = line.paint_origin_for(block.first_baseline());
516    /// let mut cursor = i64::from(block.first_rows(2));
517    /// let middle_origin = line.paint_origin_for(block.baseline_at_row(cursor));
518    /// assert!((middle_origin - (first_origin + 2.0 * line.line_height())).abs() < 1e-3);
519    /// cursor = cursor.checked_add(i64::from(block.middle_rows(4))).unwrap();
520    /// cursor = cursor.checked_add(i64::from(block.last_rows(3))).unwrap();
521    /// assert_eq!(cursor, i64::from(block.rows(9)));
522    /// ```
523    ///
524    /// # Panics
525    ///
526    /// Panics when `lines` is zero or the row count does not fit in `i32`.
527    #[inline]
528    pub fn first_rows(&self, lines: u32) -> i32 {
529        checked_rows(i128::from(self.top) + self.spanned_rows(lines))
530    }
531
532    /// Row-cursor delta across a *middle* fragment of `lines` visual lines:
533    /// `lines × line_rhythms`. The cursor points to the fragment's first
534    /// baseline before this delta and the following fragment's first baseline
535    /// after it. See [`first_rows`](Self::first_rows) for the full contract.
536    ///
537    /// # Panics
538    ///
539    /// Panics when `lines` is zero or the row count does not fit in `i32`.
540    #[inline]
541    pub fn middle_rows(&self, lines: u32) -> i32 {
542        checked_rows(self.spanned_rows(lines))
543    }
544
545    /// The baseline at an accumulated `i64` grid-row cursor. Baseline-anchored
546    /// blocks land on `row × grid size`; ink-anchored blocks retain their ink
547    /// phase.
548    ///
549    /// Keep the cursor as exact integer rows and rebase it near the viewport
550    /// before this conversion. The returned coordinate is `f32`, so accepting
551    /// `i64` avoids an arbitrary saturating cast but cannot make enormous
552    /// absolute pixel coordinates exact.
553    ///
554    /// Pass this to [`RhythmLineMetrics::paint_origin_for`] to derive the
555    /// fragment's line-box top without accumulating preceding `f32` heights.
556    #[inline]
557    pub fn baseline_at_row(&self, row: i64) -> f32 {
558        self.grid().size() * row as f32 + self.ink_ascent.unwrap_or(0.0)
559    }
560
561    /// Row-cursor delta from the first baseline of a *last* fragment of
562    /// `lines` visual lines to the block's whole-row bottom edge: `lines − 1`
563    /// line advances plus the closing for baseline anchors, or `lines`
564    /// advances plus the paired ink close for ink anchors. See
565    /// [`first_rows`](Self::first_rows) for the full contract.
566    ///
567    /// # Panics
568    ///
569    /// Panics when `lines` is zero or the row count does not fit in `i32`.
570    #[inline]
571    pub fn last_rows(&self, lines: u32) -> i32 {
572        checked_rows(self.spanned_rows(lines) + i128::from(self.bottom) - self.trailing_rows())
573    }
574
575    #[inline]
576    fn trailing_rows(&self) -> i128 {
577        if self.ink_ascent.is_some() {
578            0
579        } else {
580            i128::from(self.line.line_rhythms())
581        }
582    }
583
584    #[inline]
585    fn spanned_rows(&self, lines: u32) -> i128 {
586        assert!(lines > 0, "a fragment must contain at least one line");
587        i128::from(lines) * i128::from(self.line.line_rhythms())
588    }
589}
590
591#[inline]
592fn checked_rows(rows: i128) -> i32 {
593    i32::try_from(rows).expect("block row count exceeds i32")
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use crate::FontRhythm;
600
601    const GRID: Rhythm = Rhythm::new(8.0);
602
603    // Georgia on macOS at 16px: upem 2048, hhea 1878/-449, cap 1419.
604    fn georgia_line() -> RhythmLineMetrics {
605        RhythmLineMetrics::new(16.0 * 1878.0 / 2048.0, 16.0 * 449.0 / 2048.0, 3, GRID)
606    }
607
608    fn georgia_font() -> FontRhythm {
609        FontRhythm::from_platform_metrics(
610            16.0,
611            3,
612            16.0 * 1878.0 / 2048.0,
613            -16.0 * 449.0 / 2048.0,
614            16.0 * 1419.0 / 2048.0,
615            16.0 * 986.0 / 2048.0,
616        )
617    }
618
619    #[test]
620    fn paint_origin_solves_the_renderer_baseline_formula() {
621        // gpui paints the baseline at origin + (line_height - ascent -
622        // descent) / 2 + ascent; the origin must invert exactly that.
623        let line = georgia_line();
624        let target = GRID.height(5);
625        let origin = line.paint_origin_for(target);
626        let painted =
627            origin + (line.line_height() - line.ascent() - line.descent()) / 2.0 + line.ascent();
628        assert!((painted - target).abs() < 1e-4);
629    }
630
631    #[test]
632    fn min_line_rhythms_covers_the_metric_envelope() {
633        // Georgia's 16px ascent/descent envelope is ~18.2px: three 8px rows minimum.
634        let line = georgia_line();
635        assert_eq!(line.min_line_rhythms(), 3);
636        assert!(!line.overflows_line_box());
637
638        // An emoji-tall run on the same 3-row box overflows; four rows fit.
639        let tall = RhythmLineMetrics::new(20.0, 6.0, 3, GRID);
640        assert_eq!(tall.min_line_rhythms(), 4);
641        assert!(tall.overflows_line_box());
642        assert!(tall.half_leading() < 0.0);
643        let grown = RhythmLineMetrics::new(20.0, 6.0, 4, GRID);
644        assert!(!grown.overflows_line_box());
645
646        // Exactly filling the envelope is not an overflow, and float error within
647        // snap_up's tolerance does not claim an extra row.
648        let exact = RhythmLineMetrics::new(20.0, 4.0, 3, GRID);
649        assert_eq!(exact.min_line_rhythms(), 3);
650        assert!(!exact.overflows_line_box());
651        let noisy = RhythmLineMetrics::new(20.000_001, 4.0, 3, GRID);
652        assert_eq!(noisy.min_line_rhythms(), 3);
653    }
654
655    #[test]
656    fn at_least_grows_only_an_overflowing_line_box() {
657        let grown = RhythmLineMetrics::at_least(20.0, 6.0, 3, GRID);
658        assert_eq!(grown.line_rhythms(), 4);
659        assert!(!grown.overflows_line_box());
660
661        let roomy = RhythmLineMetrics::at_least(20.0, 6.0, 6, GRID);
662        assert_eq!(roomy.line_rhythms(), 6);
663        assert_eq!(
664            RhythmLineMetrics::at_least(georgia_line().ascent(), georgia_line().descent(), 3, GRID,),
665            georgia_line()
666        );
667    }
668
669    #[test]
670    fn line_height_preserves_the_full_u32_count() {
671        let count = i32::MAX as u32 + 1;
672        let line = RhythmLineMetrics::new(1.0, 1.0, count, GRID);
673        assert_eq!(line.line_height(), GRID.size() * count as f32);
674        assert!(line.line_height().is_sign_positive());
675    }
676
677    #[test]
678    fn overflow_is_reported_when_no_u32_line_count_can_fit_the_metric_envelope() {
679        let tiny_grid = Rhythm::new(f32::MIN_POSITIVE);
680        let line = RhythmLineMetrics::new(1.0, 0.0, u32::MAX, tiny_grid);
681        assert!(line.overflows_line_box());
682    }
683
684    #[test]
685    #[should_panic(expected = "minimum line rhythm count exceeds u32")]
686    fn minimum_line_count_rejects_a_value_outside_the_return_type() {
687        let tiny_grid = Rhythm::new(f32::MIN_POSITIVE);
688        let line = RhythmLineMetrics::new(1.0, 0.0, 1, tiny_grid);
689        let _ = line.min_line_rhythms();
690    }
691
692    #[test]
693    fn empty_shaped_lines_are_representable() {
694        let empty = RhythmLineMetrics::new(0.0, 0.0, 3, GRID);
695        assert_eq!(empty.min_line_rhythms(), 1);
696        assert!(!empty.overflows_line_box());
697        assert!((empty.baseline_above() - GRID.height(3) / 2.0).abs() < 1e-6);
698    }
699
700    #[test]
701    #[should_panic(expected = "ascent must be finite and non-negative")]
702    fn line_metrics_reject_a_negative_ascent() {
703        let _ = RhythmLineMetrics::new(-1.0, 3.0, 3, GRID);
704    }
705
706    #[test]
707    #[should_panic(expected = "line_rhythms must be greater than zero")]
708    fn line_metrics_reject_zero_rhythms() {
709        let _ = RhythmLineMetrics::new(15.0, 4.0, 0, GRID);
710    }
711
712    #[test]
713    fn baseline_anchors_match_the_style_level_spacing() {
714        // The direct-paint placement and the element-path paddings are one
715        // formula: the first line box's top sits `baseline_top(3)` below the
716        // block top, and the whole-row block closes `baseline_bottom(1)`
717        // under the last line box.
718        let line = georgia_line();
719        let block = RhythmBlockMetrics::new(line, 3, 1);
720        let font = georgia_font();
721        assert!((block.first_baseline() - GRID.height(3)).abs() < 1e-6);
722        let first_origin = line.paint_origin_for(block.first_baseline());
723        assert!((first_origin - font.baseline_top(GRID, 3)).abs() < 1e-6);
724        let closing = GRID.height(block.rows(1)) - (first_origin + line.line_height());
725        assert!((closing - font.baseline_bottom(GRID, 1)).abs() < 1e-6);
726    }
727
728    #[test]
729    fn cap_anchors_match_the_style_level_spacing() {
730        let font = georgia_font();
731        let cap_height = font.cap_height().unwrap();
732        let block = RhythmBlockMetrics::ink_anchored(georgia_line(), cap_height, 3, 0);
733        assert!((block.opening() - font.cap_top(GRID, 3).unwrap()).abs() < 1e-6);
734        assert!((block.closing() - font.cap_bottom(GRID, 0).unwrap()).abs() < 1e-6);
735        assert!((block.first_baseline() - (GRID.height(3) + cap_height)).abs() < 1e-6);
736    }
737
738    #[test]
739    fn rows_is_exact_integer_arithmetic() {
740        let block = RhythmBlockMetrics::new(georgia_line(), 3, 1);
741        for lines in [1, 2, 5, 40] {
742            assert_eq!(block.rows(lines), 3 + 1 + (lines as i32 - 1) * 3);
743            assert!((block.height(lines) - GRID.height(block.rows(lines))).abs() < 1e-3);
744        }
745
746        let ink_block = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
747        for lines in [1, 2, 5] {
748            assert_eq!(ink_block.rows(lines), 3 + lines as i32 * 3);
749            assert!((ink_block.height(lines) - GRID.height(ink_block.rows(lines))).abs() < 1e-3);
750        }
751    }
752
753    #[test]
754    fn fragment_heights_sum_to_the_whole_block() {
755        for block in [
756            RhythmBlockMetrics::new(georgia_line(), 3, 1),
757            RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
758        ] {
759            let split = block.first_height(2) + block.middle_height(4) + block.last_height(3);
760            assert!((split - block.height(9)).abs() < 1e-3);
761        }
762    }
763
764    #[test]
765    #[should_panic(expected = "block row count exceeds i32")]
766    fn rows_reject_a_count_outside_the_return_type() {
767        let line = RhythmLineMetrics::new(1.0, 1.0, u32::MAX, GRID);
768        let _ = RhythmBlockMetrics::new(line, 0, 0).rows(2);
769    }
770
771    #[test]
772    fn fragment_rows_partition_the_block_exactly() {
773        // An ink anchor closes on the last line's ink top, a baseline anchor a
774        // whole line lower, so the trailing rows differ by one line box.
775        for (block, ink_anchored) in [
776            (RhythmBlockMetrics::new(georgia_line(), 3, 1), false),
777            (
778                RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
779                true,
780            ),
781            (RhythmBlockMetrics::new(georgia_line(), -2, 1), false),
782            (
783                RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, -2, -1),
784                true,
785            ),
786        ] {
787            let line_rhythms = block.line().line_rhythms() as i32;
788            assert_eq!(
789                block.first_rows(2) + block.middle_rows(4) + block.last_rows(3),
790                block.rows(9)
791            );
792            assert_eq!(block.first_rows(1) + block.last_rows(1), block.rows(2));
793            assert_eq!(block.first_rows(5), block.top_rhythms() + 5 * line_rhythms);
794            assert_eq!(block.middle_rows(4), 4 * line_rhythms);
795            let trailing = if ink_anchored { 0 } else { line_rhythms };
796            assert_eq!(
797                block.last_rows(3),
798                3 * line_rhythms + block.bottom_rhythms() - trailing
799            );
800        }
801    }
802
803    #[test]
804    fn fragment_row_cursor_reconstructs_every_continuation_origin() {
805        for block in [
806            RhythmBlockMetrics::new(georgia_line(), 3, 1),
807            RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
808            RhythmBlockMetrics::new(georgia_line(), -2, 1),
809            RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, -2, -1),
810        ] {
811            let line = block.line();
812            // Split 2 / 4 / 1 / 3 / 2: each continuation fragment's line-box
813            // top must land where the unsplit block's next line box would —
814            // whole line advances below the first line box.
815            let mut rows = i64::from(block.first_rows(2));
816            let mut expected =
817                line.paint_origin_for(block.first_baseline()) + 2.0 * line.line_height();
818            for lines in [4, 1, 3] {
819                let origin = line.paint_origin_for(block.baseline_at_row(rows));
820                assert!((origin - expected).abs() < 1e-3);
821                rows = rows
822                    .checked_add(i64::from(block.middle_rows(lines)))
823                    .expect("test row cursor overflowed");
824                expected += line.line_height() * lines as f32;
825            }
826
827            let last_origin = line.paint_origin_for(block.baseline_at_row(rows));
828            assert!((last_origin - expected).abs() < 1e-3);
829            rows = rows
830                .checked_add(i64::from(block.last_rows(2)))
831                .expect("test row cursor overflowed");
832            assert_eq!(rows, i64::from(block.rows(2 + 4 + 1 + 3 + 2)));
833        }
834    }
835
836    #[test]
837    fn baseline_at_row_accepts_a_wide_cursor_without_i32_saturation() {
838        let block = RhythmBlockMetrics::new(georgia_line(), 3, 1);
839        let wide_row = i64::from(i32::MAX) * 4;
840        assert_eq!(
841            block.baseline_at_row(wide_row),
842            GRID.size() * wide_row as f32
843        );
844    }
845
846    #[test]
847    fn virtualizer_rebases_a_wide_multi_block_cursor_before_float_conversion() {
848        let preceding = RhythmBlockMetrics::new(georgia_line(), 3, 1);
849        let visible = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
850
851        let mut document_cursor = i64::from(i32::MAX) * 4;
852        document_cursor = document_cursor
853            .checked_add(i64::from(preceding.rows(40)))
854            .expect("test document cursor overflowed");
855        let visible_top = document_cursor;
856        document_cursor = document_cursor
857            .checked_add(i64::from(visible.rows(9)))
858            .expect("test document cursor overflowed");
859        assert!(document_cursor > i64::from(i32::MAX));
860
861        // The viewport starts two rows before the visible block. Only these
862        // small relative cursors cross the i64 -> f32 boundary, so adjacent
863        // baselines remain exact even though the document cursor is huge.
864        let viewport_origin = visible_top
865            .checked_sub(2)
866            .expect("test viewport origin overflowed");
867        let first_row = visible_top
868            .checked_add(i64::from(visible.top_rhythms()))
869            .and_then(|row| row.checked_sub(viewport_origin))
870            .expect("test first baseline cursor overflowed");
871        let continuation_row = visible_top
872            .checked_add(i64::from(visible.first_rows(2)))
873            .and_then(|row| row.checked_sub(viewport_origin))
874            .expect("test continuation cursor overflowed");
875
876        let first_baseline = visible.baseline_at_row(first_row);
877        let continuation_baseline = visible.baseline_at_row(continuation_row);
878        assert!((first_baseline - (GRID.height(2) + visible.first_baseline())).abs() < 1e-6);
879        assert!(
880            (continuation_baseline - first_baseline - 2.0 * visible.line().line_height()).abs()
881                < 1e-4
882        );
883    }
884
885    #[test]
886    fn baseline_at_row_preserves_ink_phase() {
887        let ink_block = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
888        let row = ink_block.first_rows(2);
889        let expected = ink_block.first_baseline() + 2.0 * ink_block.line().line_height();
890        assert!((ink_block.baseline_at_row(i64::from(row)) - expected).abs() < 1e-5);
891        assert!((ink_block.baseline_at_row(0) - 11.09).abs() < 1e-6);
892    }
893
894    #[test]
895    fn covering_fits_every_mixture_of_the_set_it_covers() {
896        // A body style plus the faces its runs can pull in: a monospace with
897        // a deeper descent and a display face with a far taller ascent.
898        let body = georgia_line();
899        let mono = RhythmLineMetrics::new(15.0, 6.0, 3, GRID);
900        let display = RhythmLineMetrics::new(28.8, 4.0, 3, GRID);
901        let covering = RhythmLineMetrics::covering(&[body, mono, display], GRID);
902
903        // Maxima, not any single member's pair: the worst case is a line
904        // mixing the display ascent with the monospace descent.
905        assert_eq!(covering.ascent(), 28.8);
906        assert_eq!(covering.descent(), 6.0);
907        assert_eq!(covering.line_rhythms(), 5); // A 34.8px envelope needs 5 rows.
908        assert!(!covering.overflows_line_box());
909
910        // The point of the fold: at that count no mixture overflows, so the
911        // count is a static row budget settled before anything is shaped.
912        for ascent in [body.ascent(), mono.ascent(), display.ascent()] {
913            for descent in [body.descent(), mono.descent(), display.descent()] {
914                let line = RhythmLineMetrics::new(ascent, descent, covering.line_rhythms(), GRID);
915                assert!(!line.overflows_line_box(), "{ascent} / {descent} overflows");
916            }
917        }
918    }
919
920    #[test]
921    fn covering_keeps_the_tallest_requested_line_height() {
922        // Metrics that fit everywhere: the count comes from the members, not
923        // from their envelope, and a single-member fold is the member itself.
924        let short = RhythmLineMetrics::new(14.67, 3.51, 3, GRID);
925        let tall = RhythmLineMetrics::new(10.0, 3.0, 5, GRID);
926        assert_eq!(
927            RhythmLineMetrics::covering(&[short, tall], GRID).line_rhythms(),
928            5
929        );
930        assert_eq!(RhythmLineMetrics::covering(&[short], GRID), short);
931    }
932
933    #[test]
934    #[should_panic(expected = "must cover at least one line")]
935    fn covering_rejects_an_empty_set() {
936        let _ = RhythmLineMetrics::covering(&[], GRID);
937    }
938
939    #[test]
940    #[should_panic(expected = "must be built on the covering grid")]
941    fn covering_rejects_a_line_from_another_grid() {
942        let other = RhythmLineMetrics::new(14.67, 3.51, 3, Rhythm::new(10.0));
943        let _ = RhythmLineMetrics::covering(&[georgia_line(), other], GRID);
944    }
945
946    #[test]
947    fn negative_anchor_counts_stay_meaningful_as_margins() {
948        // A zero `top` puts the line box above the block's own top edge —
949        // meaningful as a margin overlap, exactly like `baseline_top`.
950        let block = RhythmBlockMetrics::new(georgia_line(), 0, -1);
951        assert!(block.line().paint_origin_for(block.first_baseline()) < 0.0);
952        assert_eq!(block.rows(2), 0 - 1 + 3);
953    }
954
955    #[test]
956    #[should_panic(expected = "at least one line")]
957    fn fragments_reject_zero_lines() {
958        let _ = RhythmBlockMetrics::new(georgia_line(), 3, 1).rows(0);
959    }
960
961    #[test]
962    #[should_panic(expected = "ink anchor ascent must be finite and greater than zero")]
963    fn ink_anchored_blocks_reject_an_unusable_ascent() {
964        let _ = RhythmBlockMetrics::ink_anchored(georgia_line(), 0.0, 3, 0);
965    }
966}