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}