1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
//! Vertical rhythm math, independent of gpui.
//!
//! # Model
//!
//! A renderer (CSS, gpui, and most UI toolkits) vertically centers a font's
//! `ascent + descent` box inside the line box, placing the baseline at:
//!
//! ```text
//! baseline_from_top = (line_height - ascent - descent) / 2 + ascent
//! ```
//!
//! Given a line height that is an integer number of rhythm units, the baseline
//! spacing functions compute padding/margin that lands each baseline exactly on
//! a rhythm grid line. The cap-anchoring functions deliberately align the
//! capitals' ink instead and preserve whole-row block height with a paired close.
//!
//! [`Rhythm`] is only the grid: unit size, spacing, whole-row heights, and
//! snapping. Every calculation that also needs a typeface lives on
//! [`FontRhythm`], and shaped-line geometry on
//! [`RhythmLineMetrics`](crate::RhythmLineMetrics).
//!
//! All quantities are `f32` logical pixels; unit conversion (rems, device
//! scale) belongs to the integration layer.
//!
//! The Plumber/rhythm-sass `baseline-ratio` is recoverable from font metrics:
//! `ratio = (em + descent - ascent) / (2 * em)`. [`FontRhythm::from_baseline_ratio`]
//! is provided for compatibility with existing measured values.
/// The vertical rhythm grid: a stack of rows, each `size` logical pixels tall.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rhythm {
size: f32,
}
impl Rhythm {
/// Create a grid with a finite, positive rhythm-unit size.
///
/// # Panics
///
/// Panics when `size` is zero, negative, or non-finite.
pub const fn new(size: f32) -> Self {
assert!(
size.is_finite() && size > 0.0,
"rhythm unit size must be finite and greater than zero"
);
Self { size }
}
/// Height of one rhythm unit in logical pixels.
#[inline]
pub const fn size(&self) -> f32 {
self.size
}
/// Axis-neutral length of `n` rhythm units. Use this when the vertical grid
/// also supplies horizontal indents, gaps, or padding.
#[inline]
pub fn spacing(&self, n: i32) -> f32 {
self.size * n as f32
}
/// Total height of `n` rhythm units. Equivalent to rhythm-sass `rhythm($n)`;
/// apply offsets with plain addition: `grid.height(5) - 1.0`.
#[inline]
pub fn height(&self, n: i32) -> f32 {
self.spacing(n)
}
/// The smallest whole number of rhythm rows covering `height` — the pad
/// strategy for content whose height is not rhythm-controlled (images,
/// video, embeds): the block grows to the next grid line and the
/// remainder, always under one unit, becomes trailing whitespace.
///
/// Heights within a few `f32` rounding steps of a whole-row height count
/// as exact, absorbing float error from measured sizes without allowing a
/// large rhythm unit to hide a visible remainder. Unlike [`snap`], which
/// rounds a final value to the nearest step of an arbitrary size, this
/// rounds heights outward to whole rhythm rows.
///
/// # Panics
///
/// Panics when `height` is negative or non-finite.
#[inline]
pub fn snap_up(&self, height: f32) -> f32 {
self.size * self.snap_rows(height, f32::ceil)
}
/// The largest whole number of rhythm rows within `height` — the crop
/// strategy: the block shrinks to the previous grid line, cutting less
/// than one unit and never scaling the content up. Same exactness
/// tolerance as [`Self::snap_up`].
///
/// # Panics
///
/// Panics when `height` is negative or non-finite.
#[inline]
pub fn snap_down(&self, height: f32) -> f32 {
self.size * self.snap_rows(height, f32::floor)
}
// Deliberately kept separate from `RhythmLineMetrics::minimum_line_rows`,
// which applies the same rule in `f64`. The two are calibrated to their
// inputs' precision, not merely duplicated: this path divides an `f32`
// height, whose own rounding error the tolerance absorbs, so widening it
// to `f64` changes results once the tolerance approaches half a unit
// (`snap_up_keeps_a_real_remainder_at_large_heights` pins one such case),
// and bounding the tolerance instead stops it absorbing the `f32` noise
// it exists for. Change one only with a differential sweep over both.
fn snap_rows(&self, height: f32, round_away: fn(f32) -> f32) -> f32 {
assert!(
height.is_finite() && height >= 0.0,
"height must be finite and non-negative"
);
let rows = height / self.size;
let nearest = rows.round();
let nearest_height = self.size * nearest;
// Division and multiplication can each move an exact multiple by a
// few ULPs. Scale by the measured height, not by the grid size, so the
// tolerance cannot hide a visible remainder on a large grid.
let tolerance = height * f32::EPSILON * 8.0;
if (height - nearest_height).abs() <= tolerance {
nearest
} else {
round_away(rows)
}
}
}
/// Vertical metrics of one text style participating in the rhythm grid.
///
/// `ascent` and `descent` are resolved at `font_size` (logical pixels, both
/// positive). Line height is expressed in whole rhythm units, which is what keeps
/// consecutive lines of the same style on the grid.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FontRhythm {
font_size: f32,
line_rhythms: u32,
ascent: f32,
descent: f32,
cap_height: Option<f32>,
x_height: Option<f32>,
}
impl FontRhythm {
/// Build from real font metrics resolved at `font_size`.
///
/// # Panics
///
/// Panics when `font_size` or `ascent` is not finite and positive, when
/// `line_rhythms` is zero, or when `descent` is not finite and non-negative.
pub fn from_metrics(font_size: f32, line_rhythms: u32, ascent: f32, descent: f32) -> Self {
assert!(
font_size.is_finite() && font_size > 0.0,
"font_size must be finite and greater than zero"
);
assert!(line_rhythms > 0, "line_rhythms must be greater than zero");
assert!(
ascent.is_finite() && ascent > 0.0,
"ascent must be finite and greater than zero"
);
assert!(
descent.is_finite() && descent >= 0.0,
"descent must be finite and non-negative"
);
Self {
font_size,
line_rhythms,
ascent,
descent,
cap_height: None,
x_height: None,
}
}
/// Build from platform-reported metrics, normalizing conventions.
///
/// Some platforms report table metrics in the OpenType sign convention where
/// descent is negative below the baseline (gpui on macOS does), so `ascent`
/// and `descent` are taken by magnitude. Non-finite or non-positive cap and
/// x heights are treated as unavailable and become `None`.
///
/// # Panics
///
/// Panics under the same conditions as [`Self::from_metrics`] after
/// normalization (e.g. a zero `ascent`).
pub fn from_platform_metrics(
font_size: f32,
line_rhythms: u32,
ascent: f32,
descent: f32,
cap_height: f32,
x_height: f32,
) -> Self {
let mut font = Self::from_metrics(font_size, line_rhythms, ascent.abs(), descent.abs());
font.cap_height = usable_metric(cap_height);
font.x_height = usable_metric(x_height);
font
}
/// Compatibility constructor for a Plumber/rhythm-sass `baseline-ratio`
/// (`0 < ratio < 1`), using the em-box approximation the Sass library assumed:
/// `ascent = (1 - ratio) * font_size`, `descent = ratio * font_size`.
///
/// Prefer [`Self::from_metrics`]: the ratio is itself derived from metrics
/// (`(em + descent - ascent) / (2 * em)`) and the em-box model slightly
/// misplaces the baseline for fonts whose `ascent + descent != em`.
///
/// # Panics
///
/// Panics when `baseline_ratio` is not strictly between 0 and 1, when
/// `font_size` is not finite and positive, or when `line_rhythms` is zero.
pub fn from_baseline_ratio(font_size: f32, line_rhythms: u32, baseline_ratio: f32) -> Self {
assert!(
baseline_ratio > 0.0 && baseline_ratio < 1.0,
"baseline ratio must be strictly between 0 and 1"
);
Self::from_metrics(
font_size,
line_rhythms,
font_size * (1.0 - baseline_ratio),
font_size * baseline_ratio,
)
}
/// Font size in logical pixels.
#[inline]
pub const fn font_size(&self) -> f32 {
self.font_size
}
/// Line height in whole rhythm units.
#[inline]
pub const fn line_rhythms(&self) -> u32 {
self.line_rhythms
}
/// Distance from the baseline up to the top of the `ascent` box, positive.
#[inline]
pub const fn ascent(&self) -> f32 {
self.ascent
}
/// Distance from the baseline down to the bottom of the `descent` box, positive.
#[inline]
pub const fn descent(&self) -> f32 {
self.descent
}
/// Height of capital letters above the baseline, if known.
#[inline]
pub const fn cap_height(&self) -> Option<f32> {
self.cap_height
}
/// Height of a lowercase x above the baseline, if known.
#[inline]
pub const fn x_height(&self) -> Option<f32> {
self.x_height
}
/// The Plumber-style baseline ratio implied by these metrics.
#[inline]
pub fn baseline_ratio(&self) -> f32 {
(self.font_size + self.descent - self.ascent) / (2.0 * self.font_size)
}
/// The line height on `grid`: [`line_rhythms`](Self::line_rhythms) whole rhythm units.
#[inline]
pub fn line_height(&self, grid: Rhythm) -> f32 {
self.line_metrics(grid).line_height()
}
/// Extra space split above and below the `ascent + descent` box.
#[inline]
pub fn half_leading(&self, grid: Rhythm) -> f32 {
self.line_metrics(grid).half_leading()
}
/// Distance from the top of the line box down to the baseline.
#[inline]
pub fn baseline_above(&self, grid: Rhythm) -> f32 {
self.line_metrics(grid).baseline_above()
}
/// Distance from the baseline down to the bottom of the line box.
#[inline]
pub fn baseline_below(&self, grid: Rhythm) -> f32 {
self.line_metrics(grid).baseline_below()
}
/// Invisible space between the top of the line box and the cap top: the amount
/// a leading-trim (CSS `text-box-trim`) would remove. Subtract it from a top
/// spacing (or apply as negative margin) to visually butt capitals against an
/// edge or grid line.
#[inline]
pub fn cap_trim_top(&self, grid: Rhythm) -> Option<f32> {
Some(self.baseline_above(grid) - self.cap_height?)
}
/// Like [`Self::cap_trim_top`] but trimming to the x-height.
#[inline]
pub fn x_trim_top(&self, grid: Rhythm) -> Option<f32> {
Some(self.baseline_above(grid) - self.x_height?)
}
/// Spacing from an element's top edge up to the nth grid line above the first
/// baseline. Equivalent to rhythm-sass `baseline-top()` / `rhythm-bottom()`.
///
/// Applied as `padding-top` (or `margin-top`), it makes the first baseline land
/// exactly `n` rhythm units below the grid line at the element's padding edge.
///
/// The result is negative when `n × size` is smaller than
/// [`baseline_above`](Self::baseline_above); a negative value is meaningful
/// as a margin but not as a padding, so pick `n` accordingly.
#[inline]
pub fn baseline_top(&self, grid: Rhythm, n: i32) -> f32 {
grid.height(n) - self.baseline_above(grid)
}
/// Spacing from an element's bottom edge down to the nth grid line below the
/// last baseline. Equivalent to rhythm-sass `baseline-bottom()` / `rhythm-top()`.
///
/// The result is negative when `n × size` is smaller than
/// [`baseline_below`](Self::baseline_below); a negative value is meaningful
/// as a margin but not as a padding, so pick `n` accordingly.
#[inline]
pub fn baseline_bottom(&self, grid: Rhythm, n: i32) -> f32 {
grid.height(n) - self.baseline_below(grid)
}
/// Spacing from a block set in this style down to a following block set in
/// `below`, measured from the bottom of this line box to the top of the
/// below font's line box, such that the two adjacent baselines are exactly
/// `n` rhythm units apart. Equivalent to rhythm-sass `baseline-between()`.
///
/// The result is negative when `n` rhythm units cannot fit both fonts'
/// baseline distances; negative values overlap the blocks when applied.
#[inline]
pub fn baseline_between(&self, grid: Rhythm, below: &FontRhythm, n: i32) -> f32 {
self.baseline_bottom(grid, n) - below.baseline_above(grid)
}
/// Top spacing that lands the **cap ink top** — not the baseline — on the
/// nth grid line: `n × size − cap_trim_top`. The grid-woven analog of CSS
/// `text-box-trim: trim-start` with `text-box-edge: cap`, for openings
/// where the eye measures ink to edge (heroes, cards, page tops).
///
/// The block's baselines shift off the grid by `cap_height mod size`;
/// close the block with [`Self::cap_bottom`] — not
/// [`Self::baseline_bottom`] — so it still occupies a whole number of
/// rhythm rows and everything after it stays in rhythm.
///
/// `None` when this style has no usable cap height. CJK faces report one
/// anyway, so on ideographic text this anchor returns `Some` while
/// trimming to the wrong ink: ideographs are not seated on the baseline,
/// and the envelope their ink is drawn to is the ideographic character
/// face, not a cap height. Anchor those with
/// [`RhythmBlockMetrics::ink_anchored`](crate::RhythmBlockMetrics::ink_anchored),
/// or with `RhythmIcfAnchor::span` under the `gpui` feature. Worse, the
/// reported value need not describe any glyph:
/// PingFang SC publishes `sCapHeight` 0.860 em, a copy of its
/// `sTypoAscender`, while its Latin `H` actually reaches 0.714 em
/// (Hiragino Sans GB, by contrast, reports its true 0.766 em). Treat a
/// CJK face's cap and x heights as unverified.
///
/// # Examples
///
/// ```
/// use rhythm_gpui::{FontRhythm, Rhythm};
///
/// let grid = Rhythm::new(8.0);
/// // Georgia-like 28px heading on a 5-unit (40px) line.
/// let heading = FontRhythm::from_platform_metrics(28.0, 5, 25.68, -6.14, 19.40, 13.48);
///
/// // Open: the capitals' ink starts exactly on the 3rd grid line…
/// let pt = heading.cap_top(grid, 3).unwrap();
/// assert!((pt + heading.cap_trim_top(grid).unwrap() - grid.height(3)).abs() < 1e-3);
///
/// // …and the paired closer returns the trim, so the block spans whole rows.
/// let pb = heading.cap_bottom(grid, 0).unwrap();
/// let block = pt + heading.line_height(grid) + pb;
/// assert!((block - 64.0).abs() < 1e-3); // 8 whole rows: what follows stays in rhythm
/// ```
#[inline]
pub fn cap_top(&self, grid: Rhythm, n: i32) -> Option<f32> {
Some(grid.height(n) - self.cap_trim_top(grid)?)
}
/// Bottom spacing pairing [`Self::cap_top`]: `n × size + cap_trim_top`,
/// returning at the bottom exactly what `cap_top` trimmed at the top so
/// the block occupies a whole number of rhythm rows — for any number of
/// wrapped lines, since lines advance by whole rows. Equivalently, the
/// bottom edge lands `n + line_rhythms` units below the last line's cap
/// top.
///
/// `None` when this style has no usable cap height.
#[inline]
pub fn cap_bottom(&self, grid: Rhythm, n: i32) -> Option<f32> {
Some(grid.height(n) + self.cap_trim_top(grid)?)
}
/// This style's line placement on `grid` as a
/// [`RhythmLineMetrics`](crate::RhythmLineMetrics) — the same value a
/// shaped line produces, so a custom renderer can place single-style
/// text (and empty lines) through one code path.
#[inline]
pub fn line_metrics(&self, grid: Rhythm) -> crate::RhythmLineMetrics {
crate::RhythmLineMetrics::new(self.ascent, self.descent, self.line_rhythms, grid)
}
/// Solve a drop cap sunk `lines` lines deep into text set in `self`.
///
/// `cap` carries the cap face's metrics resolved at any font size (metrics
/// scale linearly, so the probe size is irrelevant); its `line_rhythms` is
/// ignored. The solved font size makes the cap face's capital span from the
/// first line's cap top down to the `lines`-th baseline, and
/// [`DropCapRhythm::top`] anchors the baseline there. A face without a
/// usable cap height falls back to the classic 0.7 em approximation, which
/// can only misplace the visual top — the baseline anchor stays exact.
///
/// Drop caps are a Latin convention; CJK paragraphs conventionally open
/// with a first-line indent instead, so there is deliberately no
/// ideographic dual of this solver. A CJK face passed as `cap` is solved
/// from its reported (Latin-glyph) cap height like any other face.
///
/// # Examples
///
/// ```
/// use rhythm_gpui::{FontRhythm, Rhythm};
///
/// let grid = Rhythm::new(8.0);
/// // Georgia-like metrics at 16px on a 3-unit (24px) line.
/// let body = FontRhythm::from_platform_metrics(16.0, 3, 14.67, -3.51, 11.09, 7.70);
/// let cap = body.drop_cap(grid, &body, 3);
///
/// // The capital spans two body lines plus the body cap height…
/// let span = 2.0 * body.line_height(grid) + body.cap_height().unwrap();
/// assert!((cap.metrics().cap_height().unwrap() - span).abs() < 1e-3);
/// // …and `top` lands its baseline exactly on the third body baseline.
/// let target = body.baseline_above(grid) + 2.0 * body.line_height(grid);
/// assert!((cap.top() + cap.metrics().baseline_above(grid) - target).abs() < 1e-3);
/// ```
///
/// # Panics
///
/// Panics when `lines` is zero or `lines × line_rhythms` overflows `u32`.
pub fn drop_cap(&self, grid: Rhythm, cap: &FontRhythm, lines: u32) -> DropCapRhythm {
assert!(lines > 0, "a drop cap must sink at least one line");
let line_rhythms = self
.line_rhythms
.checked_mul(lines)
.expect("drop cap line rhythms overflow u32");
let body_cap = self.cap_height.unwrap_or(0.7 * self.font_size);
let cap_height = cap.cap_height;
let cap_ratio = cap_height.unwrap_or(0.7 * cap.font_size) / cap.font_size;
let span = (lines - 1) as f32 * self.line_height(grid) + body_cap;
let font_size = span / cap_ratio;
let scale = font_size / cap.font_size;
let mut metrics = FontRhythm::from_metrics(
font_size,
line_rhythms,
cap.ascent * scale,
cap.descent * scale,
);
metrics.cap_height = cap_height.and_then(|h| usable_metric(h * scale));
metrics.x_height = cap.x_height.and_then(|h| usable_metric(h * scale));
let target = self.baseline_above(grid) + (lines - 1) as f32 * self.line_height(grid);
DropCapRhythm {
top: target - metrics.baseline_above(grid),
metrics,
}
}
}
/// A drop cap solved by [`FontRhythm::drop_cap`]: the cap face's metrics at the
/// solved size plus the offset anchoring its baseline.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DropCapRhythm {
metrics: FontRhythm,
top: f32,
}
impl DropCapRhythm {
/// Cap-face metrics at the solved size. The line box spans the sunk lines
/// exactly: `line_rhythms` is `lines ×` the body's `line_rhythms`.
#[inline]
pub const fn metrics(&self) -> &FontRhythm {
&self.metrics
}
/// Offset from the paragraph's top edge down to the cap's line box top,
/// negative when the cap must sit above it. Apply it visually — in a flex
/// row as a relative inset, not a margin: cap-heavy faces (cap height
/// exceeding `ascent − descent`, e.g. Merriweather) yield a positive
/// offset, and as a margin it would grow the row's cross size and push
/// everything below off the grid.
#[inline]
pub const fn top(&self) -> f32 {
self.top
}
}
fn usable_metric(height: f32) -> Option<f32> {
(height.is_finite() && height > 0.0).then_some(height)
}
/// Round `value` to the nearest multiple of `step` (e.g. `1.0 / scale_factor` to
/// snap a spacing to whole device pixels). The core functions never round, so
/// baseline-anchored results stay exact; snap only final applied values.
///
/// # Panics
///
/// Panics when `step` is zero, negative, or non-finite.
pub fn snap(value: f32, step: f32) -> f32 {
assert!(
step.is_finite() && step > 0.0,
"snap step must be finite and greater than zero"
);
(value / step).round() * step
}
#[cfg(test)]
mod tests {
use super::*;
const GRID: Rhythm = Rhythm::new(8.0);
const NOTO_SERIF_RATIO: f32 = 0.112061;
// Mirrors __tests__/_font-maps.scss from rhythm-sass.
fn font_map_3() -> FontRhythm {
FontRhythm::from_baseline_ratio(17.0, 3, NOTO_SERIF_RATIO)
}
fn font_map_3_with_cap(cap_height: f32) -> FontRhythm {
let base = font_map_3();
FontRhythm::from_platform_metrics(
base.font_size(),
base.line_rhythms(),
base.ascent(),
base.descent(),
cap_height,
base.x_height().unwrap_or(0.0),
)
}
fn font_map_5() -> FontRhythm {
FontRhythm::from_baseline_ratio(12.0, 2, NOTO_SERIF_RATIO)
}
#[test]
fn rhythm_spacing_and_height() {
assert_eq!(GRID.spacing(5), 40.0);
assert_eq!(GRID.spacing(0), 0.0);
assert_eq!(GRID.spacing(-1), -8.0);
assert_eq!(GRID.height(5), 40.0);
assert_eq!(GRID.height(5), GRID.spacing(5));
assert_eq!(GRID.height(5) - 1.0, 39.0); // offsets are plain addition
assert_eq!(GRID.height(0), 0.0);
assert_eq!(GRID.height(-1), -8.0);
}
// rhythm-sass rounded baseline offsets to whole CSS px before subtracting
// (see calc-baseline-offset in _lib.scss). The math here stays exact, so the
// Sass parity tests apply the same rounding explicitly.
fn sass_baseline_bottom(font: &FontRhythm, n: i32) -> f32 {
GRID.height(n) - font.baseline_below(GRID).round()
}
fn sass_baseline_top(font: &FontRhythm, n: i32) -> f32 {
GRID.height(n) - font.baseline_above(GRID).round()
}
#[test]
fn sass_parity_rhythm_top_and_bottom() {
// Expected values from __tests__/rhythm.test.scss.
assert_eq!(sass_baseline_bottom(&font_map_3(), 3), 19.0); // rhythm-top
assert_eq!(sass_baseline_bottom(&font_map_3(), 0), -5.0);
assert_eq!(sass_baseline_bottom(&font_map_3(), -1), -13.0);
assert_eq!(sass_baseline_top(&font_map_3(), 3), 5.0); // rhythm-bottom
assert_eq!(sass_baseline_top(&font_map_3(), 0), -19.0);
assert_eq!(sass_baseline_top(&font_map_3(), -1), -27.0);
}
#[test]
fn sass_parity_baseline_between() {
let above = font_map_3();
let below = font_map_5();
let result = sass_baseline_bottom(&above, 3) - below.baseline_above(GRID).round();
assert_eq!(result, 6.0);
}
#[test]
fn exact_functions_land_baseline_on_grid() {
let font = font_map_3();
// padding_top + renderer's baseline placement = exact grid multiple
let padding_top = font.baseline_top(GRID, 3);
assert!((padding_top + font.baseline_above(GRID) - GRID.height(3)).abs() < 1e-4);
let padding_bottom = font.baseline_bottom(GRID, 3);
assert!((padding_bottom + font.baseline_below(GRID) - GRID.height(3)).abs() < 1e-4);
// Two stacked blocks: distance between adjacent baselines is n grid units.
let below = font_map_5();
let gap = font.baseline_between(GRID, &below, 3);
let baseline_distance = font.baseline_below(GRID) + gap + below.baseline_above(GRID);
assert!((baseline_distance - GRID.height(3)).abs() < 1e-4);
}
#[test]
fn baseline_ratio_roundtrip() {
let font = font_map_3();
assert!((font.baseline_ratio() - NOTO_SERIF_RATIO).abs() < 1e-6);
}
#[test]
fn baseline_ratio_from_real_metrics_matches_plumber() {
// Noto Serif: units_per_em 1000, hhea ascent 1069, descent 293.
// Plumber's published ratio for Noto Serif is 0.112061.
let em = 1000.0;
let font = FontRhythm::from_metrics(em, 1, 1069.0, 293.0);
assert!((font.baseline_ratio() - 0.112).abs() < 1e-3);
}
#[test]
fn platform_metrics_normalize_opentype_signs() {
// gpui's FontMetrics reports table descent as negative on macOS
// (OpenType sign convention); magnitudes must come out positive.
let font = FontRhythm::from_platform_metrics(16.0, 3, 14.75, -3.25, 11.2, 8.1);
assert_eq!(font.ascent(), 14.75);
assert_eq!(font.descent(), 3.25);
assert_eq!(font.cap_height(), Some(11.2));
assert_eq!(font.x_height(), Some(8.1));
}
#[test]
fn platform_metrics_filter_unusable_optional_heights() {
let font = FontRhythm::from_platform_metrics(16.0, 3, 14.75, 3.25, 0.0, f32::NAN);
assert_eq!(font.cap_height(), None);
assert_eq!(font.x_height(), None);
let font = FontRhythm::from_platform_metrics(16.0, 3, 14.75, 3.25, -1.0, 7.5);
assert_eq!(font.cap_height(), None);
assert_eq!(font.x_height(), Some(7.5));
}
/// Pins CHANGELOG.md's 0.2 migration recipe: the documented em-box
/// composition must stay bit-identical to `from_baseline_ratio`.
#[test]
fn changelog_ratio_recipe_matches_from_baseline_ratio() {
let composed = FontRhythm::from_platform_metrics(
17.0,
3,
17.0 * (1.0 - NOTO_SERIF_RATIO),
17.0 * NOTO_SERIF_RATIO,
11.9,
8.5,
);
let plain = font_map_3();
assert_eq!(composed.font_size(), plain.font_size());
assert_eq!(composed.line_rhythms(), plain.line_rhythms());
assert_eq!(composed.ascent(), plain.ascent());
assert_eq!(composed.descent(), plain.descent());
assert_eq!(composed.baseline_ratio(), plain.baseline_ratio());
// …while carrying the heights a bare ratio cannot express.
assert_eq!(composed.cap_height(), Some(11.9));
assert_eq!(composed.x_height(), Some(8.5));
assert_eq!(plain.cap_height(), None);
}
#[test]
#[should_panic(expected = "line_rhythms must be greater than zero")]
fn zero_line_rhythms_are_rejected() {
let _ = FontRhythm::from_metrics(16.0, 0, 14.75, 3.25);
}
#[test]
#[should_panic(expected = "baseline ratio must be strictly between 0 and 1")]
fn out_of_range_baseline_ratio_is_rejected() {
let _ = FontRhythm::from_baseline_ratio(16.0, 3, 1.5);
}
#[test]
fn cap_trim() {
let font = font_map_3_with_cap(0.7 * 17.0);
let trim = font.cap_trim_top(GRID).unwrap();
assert!((trim - (font.baseline_above(GRID) - 11.9)).abs() < 1e-4);
assert_eq!(font_map_3().cap_trim_top(GRID), None);
}
#[test]
fn snap_to_device_pixels() {
assert_eq!(snap(5.4, 0.5), 5.5); // 2x display
assert_eq!(snap(5.4, 1.0), 5.0);
assert_eq!(snap(-5.4, 0.5), -5.5);
}
#[test]
fn snap_heights_to_whole_rows() {
assert_eq!(GRID.snap_up(450.0), 456.0); // 800px wide at 16:9
assert_eq!(GRID.snap_down(450.0), 448.0);
assert_eq!(GRID.snap_up(0.0), 0.0);
assert_eq!(GRID.snap_down(7.9), 0.0); // under one row floors to zero
// Exact multiples pass through both directions.
assert_eq!(GRID.snap_up(448.0), 448.0);
assert_eq!(GRID.snap_down(448.0), 448.0);
}
#[test]
fn snap_heights_absorb_float_error_near_whole_rows() {
assert_eq!(GRID.snap_up(448.0002), 448.0);
assert_eq!(GRID.snap_down(447.9998), 448.0);
}
#[test]
fn snap_up_keeps_a_real_remainder_at_large_heights() {
// Guards the `f32` calibration of `snap_rows`: at this magnitude the
// proportional tolerance (~4.25) approaches half the 8.5 unit, and
// computing the same rule in `f64` instead swallows the genuine 4px
// remainder and returns the row below. 4460404.5 / 8.5 is
// 524753.47..., so snapping up must reach row 524754.
let grid = Rhythm::new(8.5);
assert_eq!(grid.snap_up(4_460_404.5), 8.5 * 524_754.0);
assert_eq!(grid.snap_down(4_460_404.5), 8.5 * 524_753.0);
}
#[test]
fn snap_tolerance_does_not_scale_with_the_grid_size() {
let large_grid = Rhythm::new(1_000_000.0);
assert_eq!(large_grid.snap_up(50.0), 1_000_000.0);
assert_eq!(large_grid.snap_down(50.0), 0.0);
}
#[test]
#[should_panic(expected = "height must be finite and non-negative")]
fn snap_heights_reject_negative_values() {
let _ = GRID.snap_up(-1.0);
}
// Georgia on macOS: upem 2048, hhea 1878/-449, cap 1419, x 986.
fn georgia_16() -> FontRhythm {
FontRhythm::from_platform_metrics(
16.0,
3,
16.0 * 1878.0 / 2048.0,
-16.0 * 449.0 / 2048.0,
16.0 * 1419.0 / 2048.0,
16.0 * 986.0 / 2048.0,
)
}
#[test]
fn drop_cap_baseline_lands_on_the_sunk_line() {
let body = georgia_16();
let cap = body.drop_cap(GRID, &body, 3);
assert_eq!(cap.metrics().line_rhythms(), 9);
// The capital spans two body lines plus the body cap height…
let span = 2.0 * body.line_height(GRID) + body.cap_height().unwrap();
assert!((cap.metrics().cap_height().unwrap() - span).abs() < 1e-3);
// …and its baseline lands exactly on the third body baseline.
let target = body.baseline_above(GRID) + 2.0 * body.line_height(GRID);
assert!((cap.top() + cap.metrics().baseline_above(GRID) - target).abs() < 1e-3);
}
#[test]
fn drop_cap_sizes_by_the_cap_faces_own_metrics() {
let body = georgia_16();
// A display face with taller capitals: cap height 0.8 em.
let display = FontRhythm::from_platform_metrics(12.0, 1, 11.0, -3.0, 9.6, 6.0);
let cap = body.drop_cap(GRID, &display, 3);
let span = 2.0 * body.line_height(GRID) + body.cap_height().unwrap();
// Solved with the cap face's own ratio, not the body's.
assert!((cap.metrics().font_size() - span / 0.8).abs() < 1e-3);
assert!((cap.metrics().cap_height().unwrap() - span).abs() < 1e-3);
}
#[test]
fn cap_heavy_faces_need_a_positive_top_offset() {
// Merriweather: upem 2000, hhea 1968/-546, cap 1486. Cap height
// (0.743 em) exceeds ascent − descent (0.711 em), which flips the
// anchor offset positive — the reason the offset must be applied as a
// relative inset, not a margin (a positive margin grows the flex row's
// cross size and pushes everything below off the grid).
let body = FontRhythm::from_platform_metrics(
16.0,
3,
16.0 * 1968.0 / 2000.0,
-16.0 * 546.0 / 2000.0,
16.0 * 1486.0 / 2000.0,
16.0 * 1111.0 / 2000.0,
);
let cap = body.drop_cap(GRID, &body, 3);
assert!(
cap.top() > 1.0 && cap.top() < 1.1,
"expected ≈ +1.03, got {}",
cap.top()
);
let target = body.baseline_above(GRID) + 2.0 * body.line_height(GRID);
assert!((cap.top() + cap.metrics().baseline_above(GRID) - target).abs() < 1e-3);
}
#[test]
fn drop_cap_falls_back_to_the_em_approximation() {
let body = font_map_3(); // baseline-ratio construction: no cap height
let cap = body.drop_cap(GRID, &body, 2);
let expected_size = (body.line_height(GRID) + 0.7 * body.font_size()) / 0.7;
assert!((cap.metrics().font_size() - expected_size).abs() < 1e-3);
// The fallback is not fabricated into the solved metrics.
assert_eq!(cap.metrics().cap_height(), None);
// The baseline anchor holds regardless.
let target = body.baseline_above(GRID) + body.line_height(GRID);
assert!((cap.top() + cap.metrics().baseline_above(GRID) - target).abs() < 1e-3);
}
#[test]
#[should_panic(expected = "at least one line")]
fn drop_cap_rejects_zero_lines() {
let _ = font_map_3().drop_cap(GRID, &font_map_3(), 0);
}
#[test]
fn cap_pair_opens_on_ink_and_closes_on_whole_rows() {
// Georgia-like 28px heading on a 5-unit line.
let heading = FontRhythm::from_platform_metrics(
28.0,
5,
28.0 * 1878.0 / 2048.0,
-28.0 * 449.0 / 2048.0,
28.0 * 1419.0 / 2048.0,
28.0 * 986.0 / 2048.0,
);
let pt = heading.cap_top(GRID, 3).unwrap();
// The capitals' ink starts exactly on the 3rd grid line…
assert!((pt + heading.cap_trim_top(GRID).unwrap() - GRID.height(3)).abs() < 1e-3);
// …and the paired closer makes the block a whole number of rows.
let pb = heading.cap_bottom(GRID, 0).unwrap();
let block = pt + heading.line_height(GRID) + pb;
assert!((block - 64.0).abs() < 1e-3);
// Closing with baseline_bottom instead would leave the fractional
// cap-height phase inside the block and push everything below it
// off the grid.
let mixed = pt + heading.line_height(GRID) + heading.baseline_bottom(GRID, 1);
let rows = mixed / GRID.size();
assert!((rows - rows.round()).abs() > 0.01);
}
#[test]
fn cap_anchors_need_a_usable_cap_height() {
let no_cap = font_map_3(); // baseline-ratio construction: no cap height
assert_eq!(no_cap.cap_top(GRID, 3), None);
assert_eq!(no_cap.cap_bottom(GRID, 1), None);
}
// PingFang SC Regular, read from the font file: upem 1000, hhea
// 1060/-340, BASE icfb -102 / icft +822, OS/2 sCapHeight 860 and sxHeight
// 600. Those last two are placeholders, not measurements — sCapHeight
// simply repeats sTypoAscender, while the face's real H reaches 0.714 em
// and its x 0.517 em. The fixtures keep the reported values because they
// are what a text system hands back.
const PINGFANG_ICFT: f32 = 0.822;
fn pingfang(size: f32, rows: u32) -> FontRhythm {
FontRhythm::from_platform_metrics(
size,
rows,
1.060 * size,
-0.340 * size,
0.860 * size,
0.600 * size,
)
}
/// The math layer needs no ideographic anchor of its own:
/// [`RhythmBlockMetrics::ink_anchored`] takes the anchored ink height, so
/// passing a character-face ascent lands ideographic ink and still spans
/// whole rows.
#[test]
fn ideographic_ink_anchors_through_the_generic_block_metrics() {
let font = pingfang(16.0, 3);
let icf = PINGFANG_ICFT * 16.0;
let line = font.line_metrics(GRID);
let block = crate::RhythmBlockMetrics::ink_anchored(line, icf, 3, 0);
// The character face's top edge starts exactly on the 3rd grid line…
let trim = font.baseline_above(GRID) - icf;
assert!((block.opening() + trim - GRID.height(3)).abs() < 1e-3);
// …and the paired closer makes the block a whole number of rows.
assert!((block.height(1) - 48.0).abs() < 1e-3);
assert_eq!(block.rows(1), 6);
// Closing with a baseline anchor instead would leave the fractional
// character-face phase inside the block.
let mixed = block.opening() + font.line_height(GRID) + font.baseline_bottom(GRID, 1);
let rows = mixed / GRID.size();
assert!((rows - rows.round()).abs() > 0.01);
}
#[test]
fn a_cjk_faces_cap_height_is_the_wrong_ink() {
// Two hazards at once. The reported cap height anchors Latin ink that
// ideographs do not share, and PingFang's reported value (0.860 em,
// a copy of sTypoAscender) describes no glyph at all — its real H is
// 0.714 em. Meanwhile the em box top, 0.86 em, sits above every
// ideograph's ink (字 reaches +0.825 em), so anchoring the box leaves
// a visible gap where the character face does not.
let font = pingfang(16.0, 3);
let icf = PINGFANG_ICFT * 16.0;
let ink_top = 0.825 * 16.0;
let cap_anchor = font.cap_top(GRID, 3).unwrap();
let face_anchor =
crate::RhythmBlockMetrics::ink_anchored(font.line_metrics(GRID), icf, 3, 0).opening();
assert!(
face_anchor < cap_anchor,
"the character face sits above the reported cap height"
);
// The face anchor puts real ink within a fraction of a pixel of the
// grid line; an em-box anchor is a visible distance short of it.
assert!((icf - ink_top).abs() < 0.06);
assert!(0.86 * 16.0 - ink_top > 0.5);
}
/// Pins the worked example in README.md's CJK section.
#[test]
fn readme_heading_flush_to_a_card_edge() {
// PingFang SC 24px on a 5-unit (40px) line, 8px grid.
let heading = pingfang(24.0, 5);
let icf = PINGFANG_ICFT * 24.0;
let trim = heading.baseline_above(GRID) - icf;
assert!((trim - 8.912).abs() < 1e-3, "invisible space, got {trim}");
// The naive padding overshoots by that whole invisible band.
assert!((16.0 + trim - 24.912).abs() < 1e-3);
// The anchored pair lands the ink and still spans whole rows.
let block = crate::RhythmBlockMetrics::ink_anchored(heading.line_metrics(GRID), icf, 2, 0);
assert!((block.opening() - 7.088).abs() < 1e-3);
assert!((block.opening() + trim - 16.0).abs() < 1e-3);
assert!((block.height(1) - 56.0).abs() < 1e-3);
assert_eq!(block.rows(1), 7);
// The cap anchor misses by PingFang's reported cap height.
assert!((heading.cap_top(GRID, 2).unwrap() + trim - 16.912).abs() < 1e-3);
}
}