Skip to main content

rhythm_gpui/
integration.rs

1//! The gpui integration: metric resolution through `TextSystem`, `Pixels`-typed
2//! spacing, drop caps, the `RhythmStyled` extension, and the debug overlay.
3
4use gpui::{
5    canvas, fill, point, px, rgba, size, App, Bounds, Font, FontId, Hsla, IntoElement,
6    ParentElement, Pixels, RenderOnce, Styled, TextSystem, Window,
7};
8
9use crate::{FontRhythm, Rhythm, RhythmBlockMetrics, RhythmLineMetrics};
10
11const DEFAULT_RHYTHM_OVERLAY_RGBA: u32 = 0xff78783f;
12
13fn assert_valid_font_request(font: &Font, font_size: Pixels, line_rhythms: u32) {
14    assert!(
15        font.weight.0.is_finite() && font.weight.0 > 0.0,
16        "font weight must be finite and greater than zero"
17    );
18    let font_size: f32 = font_size.into();
19    assert!(
20        font_size.is_finite() && font_size > 0.0,
21        "font_size must be finite and greater than zero"
22    );
23    assert!(line_rhythms > 0, "line_rhythms must be greater than zero");
24}
25
26/// Extract the above-baseline edge from a full-frame ideographic glyph.
27/// gpui's platform backends report glyph-space bounds with the origin at the
28/// ink bottom and positive height toward its top, so requiring the ink to
29/// straddle the alphabetic baseline rejects both unsuitable probes and gpui
30/// 0.2.2's Linux advance-only placeholder bounds.
31fn ideographic_ink_ascent(bounds: Bounds<Pixels>) -> Option<f32> {
32    let bottom = f32::from(bounds.origin.y);
33    let top = f32::from(bounds.origin.y + bounds.size.height);
34    (bottom.is_finite() && bottom < 0.0 && top.is_finite() && top > 0.0).then_some(top)
35}
36
37fn select_icf_ascent<E>(
38    bounds: impl IntoIterator<Item = Result<Bounds<Pixels>, E>>,
39) -> Result<f32, IcfMeasurementError> {
40    let mut saw_bounds = false;
41    let mut ascent: Option<f32> = None;
42
43    for bounds in bounds {
44        let Ok(bounds) = bounds else {
45            continue;
46        };
47        saw_bounds = true;
48
49        if let Some(candidate) = ideographic_ink_ascent(bounds) {
50            ascent = Some(ascent.map_or(candidate, |current| current.max(candidate)));
51        }
52    }
53
54    match ascent {
55        Some(ascent) => Ok(ascent),
56        None if saw_bounds => Err(IcfMeasurementError::NoUsableBounds),
57        None => Err(IcfMeasurementError::NoProbeBounds),
58    }
59}
60
61/// The vertical rhythm grid in gpui units.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct RhythmGrid {
64    core: Rhythm,
65}
66
67impl RhythmGrid {
68    /// Create a grid with a finite, positive rhythm-unit size.
69    ///
70    /// # Panics
71    ///
72    /// Panics when `size` is zero, negative, or non-finite.
73    pub fn new(size: Pixels) -> Self {
74        Self {
75            core: Rhythm::new(size.into()),
76        }
77    }
78
79    /// Height of one rhythm unit.
80    #[inline]
81    pub fn size(&self) -> Pixels {
82        px(self.core.size())
83    }
84
85    /// This grid as the dependency-free [`Rhythm`] — the entry to the math
86    /// layer, mirroring [`RhythmFont::metrics`] for fonts. Use it to hand this
87    /// grid to the `f32` layer: [`FontRhythm`] geometry takes it per call, and
88    /// [`RhythmLineMetrics`] takes it once at construction.
89    #[inline]
90    pub const fn rhythm(&self) -> Rhythm {
91        self.core
92    }
93
94    /// Axis-neutral length of `n` rhythm units. Use this for horizontal
95    /// indents, gaps, or padding measured with the same scale as the vertical
96    /// rhythm.
97    #[inline]
98    pub fn spacing(&self, n: i32) -> Pixels {
99        px(self.rhythm().spacing(n))
100    }
101
102    /// Total height of `n` rhythm units (rhythm-sass `rhythm($n)`). An exact
103    /// alias for [`spacing`](Self::spacing), kept as the vertical name.
104    #[inline]
105    pub fn height(&self, n: i32) -> Pixels {
106        self.spacing(n)
107    }
108
109    /// Round `height` up to whole rhythm rows — the pad strategy for content
110    /// whose height is not rhythm-controlled; see [`Rhythm::snap_up`]. With a
111    /// known width this is all a media block needs:
112    /// `div().w(w).h(grid.snap_up(w / ratio))`; for fluid widths use
113    /// [`rhythm_frame`](crate::rhythm_frame).
114    ///
115    /// # Panics
116    ///
117    /// Panics when `height` is negative or non-finite.
118    #[inline]
119    pub fn snap_up(&self, height: Pixels) -> Pixels {
120        px(self.rhythm().snap_up(height.into()))
121    }
122
123    /// Round `height` down to whole rhythm rows — the crop strategy; see
124    /// [`Rhythm::snap_down`].
125    ///
126    /// # Panics
127    ///
128    /// Panics when `height` is negative or non-finite.
129    #[inline]
130    pub fn snap_down(&self, height: Pixels) -> Pixels {
131        px(self.rhythm().snap_down(height.into()))
132    }
133
134    /// Metrics for one shaped line on this grid — the `Pixels`-typed entry to
135    /// [`RhythmLineMetrics`]. Feed a `WrappedLine`'s `ascent()` / `descent()`
136    /// (the shaped maxima over the line's explicit font runs) and the line
137    /// height in whole rhythm units; see [`RhythmLineMetrics`] for the
138    /// placement contract.
139    pub fn line_metrics(
140        &self,
141        ascent: Pixels,
142        descent: Pixels,
143        line_rhythms: u32,
144    ) -> RhythmLineMetrics {
145        RhythmLineMetrics::new(ascent.into(), descent.into(), line_rhythms, self.rhythm())
146    }
147
148    /// [`line_metrics`](Self::line_metrics) with `line_rhythms` as a floor:
149    /// grow the line box when the reported ascent/descent envelope needs more
150    /// rows.
151    pub fn line_metrics_at_least(
152        &self,
153        ascent: Pixels,
154        descent: Pixels,
155        line_rhythms: u32,
156    ) -> RhythmLineMetrics {
157        RhythmLineMetrics::at_least(ascent.into(), descent.into(), line_rhythms, self.rhythm())
158    }
159
160    /// The smallest line box on this grid containing every line in `metrics`.
161    ///
162    /// # Panics
163    ///
164    /// Panics when `metrics` is empty or an entry was built on another grid.
165    pub fn line_metrics_covering(&self, metrics: &[RhythmLineMetrics]) -> RhythmLineMetrics {
166        RhythmLineMetrics::covering(metrics, self.rhythm())
167    }
168
169    /// Resolve a font bound to this grid — [`RhythmFont::resolve`] with the
170    /// grid slot filled in; see it for the fallback-resolution caveats.
171    pub fn font(
172        &self,
173        text_system: &TextSystem,
174        font: Font,
175        font_size: Pixels,
176        line_rhythms: u32,
177    ) -> RhythmFont {
178        RhythmFont::resolve(text_system, font, font_size, line_rhythms, *self)
179    }
180
181    /// A debug overlay on this grid in a custom `color` — [`rhythm_overlay`]
182    /// as a grid factory. Pass the result to
183    /// [`RhythmStyled::rhythm_debug_overlay`] in place of the bare grid to
184    /// customize the stripes without giving up the chainable toggle.
185    pub fn overlay(&self, color: impl Into<Hsla>) -> RhythmOverlay {
186        rhythm_overlay(*self, color)
187    }
188}
189
190/// `Pixels`-typed mirrors of the two line values that stay inside a paint
191/// path's `Pixels` chain: both reach `WrappedLine::paint`.
192///
193/// Only four values across this type and [`RhythmBlockMetrics`] are mirrored.
194/// The rest of the `f32` surface is read once and converted once, so it is not
195/// mirrored: `px(line.ascent())` at the call site is one conversion, while a
196/// mirror per accessor doubles the surface to save it. Row counts are never
197/// mirrored because they identify grid rows rather than pixel lengths.
198impl RhythmLineMetrics {
199    /// [`line_height`](Self::line_height) in `Pixels` — the value
200    /// `WrappedLine::paint` takes.
201    #[inline]
202    pub fn line_height_px(&self) -> Pixels {
203        px(self.line_height())
204    }
205
206    /// [`paint_origin_for`](Self::paint_origin_for) in `Pixels` — the
207    /// `origin.y` for `WrappedLine::paint`, from a `Pixels` target baseline.
208    #[inline]
209    pub fn paint_origin_for_px(&self, target_baseline: Pixels) -> Pixels {
210        px(self.paint_origin_for(target_baseline.into()))
211    }
212}
213
214/// `Pixels`-typed mirrors of the two block values that stay inside a paint
215/// path's `Pixels` chain: both are summed with grid lengths into the target
216/// baseline [`RhythmLineMetrics::paint_origin_for_px`] consumes. See those
217/// mirrors for the integration-boundary rule.
218impl RhythmBlockMetrics {
219    /// [`first_baseline`](Self::first_baseline) in `Pixels`.
220    #[inline]
221    pub fn first_baseline_px(&self) -> Pixels {
222        px(self.first_baseline())
223    }
224
225    /// [`baseline_at_row`](Self::baseline_at_row) in `Pixels`.
226    #[inline]
227    pub fn baseline_at_row_px(&self, row: i64) -> Pixels {
228        px(self.baseline_at_row(row))
229    }
230}
231
232/// A requested gpui font bound to the rhythm grid, with vertical metrics from
233/// the font gpui actually resolved. When the requested family is unavailable,
234/// that may be a fallback font; see [`Self::resolve`].
235///
236/// # Lifecycle
237///
238/// A `RhythmFont` is an immutable resolved value. [`Self::resolve`] touches
239/// gpui's `TextSystem`; optional ideographic-character-face measurement reads
240/// it separately through [`Self::measure_icf`] and returns a
241/// [`RhythmIcfAnchor`] without changing this value. Metric and spacing methods
242/// are pure geometry on the stored values, allocation-free and lock-free,
243/// while style application reuses the stored [`Font`] without querying the
244/// text system. Register each font family
245/// (`TextSystem::add_fonts`) *before its first resolution*: gpui caches failed
246/// lookups by [`Font`], so adding a family later and clearing a caller-owned
247/// cache does not repair that miss in the same `TextSystem`. Resolution
248/// silently falls back otherwise. Re-resolve affected values when typography
249/// settings produce a new request key; nothing revalidates an existing value
250/// against the text system. The crate keeps no font cache of its own: gpui
251/// already caches `Font → FontId` and metrics, so a caller wanting to reuse
252/// resolved values owns that map, keyed by [`RhythmFontSpec`], along with its
253/// invalidation.
254#[derive(Debug, Clone)]
255pub struct RhythmFont {
256    font: Font,
257    font_id: Option<FontId>,
258    metrics: FontRhythm,
259    grid: RhythmGrid,
260}
261
262/// Why an ideographic character-face measurement could not produce an anchor.
263///
264/// The variants describe what was observable through gpui's public API, not a
265/// guessed platform cause. A backend may still substitute a missing glyph —
266/// DirectWrite returns `.notdef` in gpui 0.2.2 — so successful measurement also
267/// depends on supplying probes covered by the resolved face.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269#[non_exhaustive]
270pub enum IcfMeasurementError {
271    /// The [`RhythmFont`] was synthesized without a gpui [`TextSystem`] and
272    /// therefore has no resolved [`FontId`] to measure.
273    UnresolvedFont,
274    /// The probe string was empty.
275    EmptyProbes,
276    /// gpui returned an error instead of bounds for every probe.
277    ///
278    /// On gpui 0.2.2's CoreText and Linux backends, this is the result when the
279    /// resolved face covers none of the supplied characters. Other backend
280    /// errors can produce the same observable result.
281    NoProbeBounds,
282    /// At least one probe returned bounds, but none were finite and spanned the
283    /// alphabetic baseline.
284    ///
285    /// This rejects both unsuitable glyphs and gpui 0.2.2's Linux
286    /// advance-only placeholder bounds.
287    NoUsableBounds,
288}
289
290impl std::fmt::Display for IcfMeasurementError {
291    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        formatter.write_str(match self {
293            Self::UnresolvedFont => "the rhythm font has no TextSystem-resolved font identity",
294            Self::EmptyProbes => "no ICF probe glyphs were supplied",
295            Self::NoProbeBounds => "the text backend returned no bounds for any ICF probe",
296            Self::NoUsableBounds => {
297                "probe bounds were returned, but none described usable glyph ink"
298            }
299        })
300    }
301}
302
303impl std::error::Error for IcfMeasurementError {}
304
305/// A measured ideographic character-face anchor bound to the exact
306/// [`RhythmFont`] it was measured from.
307///
308/// This capability exists only after successful [`RhythmFont::measure_icf`],
309/// so its spacing operations are infallible and its ascent cannot be paired
310/// with a different face, size, or rhythm grid. Cap height, when present,
311/// arrives with a resolved [`RhythmFont`] and therefore exposes
312/// [`RhythmFont::cap_span`] there; an ICF ascent depends on caller-selected
313/// probes, which is why its `span` lives on this measured capability instead.
314#[derive(Debug, Clone)]
315pub struct RhythmIcfAnchor {
316    font: RhythmFont,
317    ascent: Pixels,
318}
319
320impl RhythmIcfAnchor {
321    /// The resolved font this anchor was measured from.
322    #[inline]
323    pub const fn font(&self) -> &RhythmFont {
324        &self.font
325    }
326
327    /// The paired ideographic-ink opening and closing.
328    ///
329    /// The opening lands the character face's top edge on the `top`th grid
330    /// line. The closing hands the trimmed band back so the block still spans
331    /// whole rhythm rows for any number of wrapped lines.
332    ///
333    /// A renderer that already has a trusted character-face ascent — or cannot
334    /// measure one through gpui — can call
335    /// [`RhythmBlockMetrics::ink_anchored`] directly.
336    #[inline]
337    pub fn span(&self, top: i32, bottom: i32) -> (Pixels, Pixels) {
338        let block = RhythmBlockMetrics::ink_anchored(
339            self.font.line_metrics(),
340            f32::from(self.ascent),
341            top,
342            bottom,
343        );
344        (px(block.opening()), px(block.closing()))
345    }
346
347    /// The invisible band between the line box's top edge and the measured
348    /// ideographic character face.
349    #[inline]
350    pub fn trim_top(&self) -> Pixels {
351        self.font.baseline_above() - self.ascent
352    }
353}
354
355impl RhythmFont {
356    /// Resolve `font`'s metrics at `font_size` through gpui's text system.
357    ///
358    /// If gpui cannot load the requested font, its [`TextSystem::resolve_font`]
359    /// silently tries the configured fallback stack. The returned value retains
360    /// the requested [`Font`] configuration, while its metrics come from the
361    /// resolved fallback; applying it through [`RhythmStyled::rhythm_font`]
362    /// follows gpui's same resolution policy. Check
363    /// [`TextSystem::all_font_names`] before calling this method when using the
364    /// exact family is a requirement.
365    ///
366    /// Metrics come from the resolved primary font only — what an element
367    /// that sets one text style paints with. A line that explicitly mixes
368    /// fonts (bold/italic runs, inline code, an explicit CJK or emoji face)
369    /// has one baseline placed from the maximum ascent and descent over its
370    /// runs; shape it and place the result through
371    /// [`RhythmLineMetrics`](crate::RhythmLineMetrics) so that shared
372    /// baseline still lands on the grid. Glyph-level fallback is different:
373    /// on the validated macOS/CoreText backend, substituted glyphs borrow the
374    /// primary font's baseline and do not enlarge the shaped line's
375    /// `ascent`/`descent`. That is a line-metrics contract, not proof that every
376    /// fallback glyph's typographic or raster ink stays inside the primary line
377    /// box; other gpui text backends need their own native validation.
378    ///
379    /// # Panics
380    ///
381    /// Panics when the font weight is zero, negative, or non-finite, when
382    /// `font_size` is zero, negative, or non-finite, or when `line_rhythms` is
383    /// zero. Validation happens before the text system is queried.
384    pub fn resolve(
385        text_system: &TextSystem,
386        font: Font,
387        font_size: Pixels,
388        line_rhythms: u32,
389        grid: RhythmGrid,
390    ) -> Self {
391        assert_valid_font_request(&font, font_size, line_rhythms);
392        let font_id = text_system.resolve_font(&font);
393        // gpui's FontMetrics keeps the OpenType sign convention where descent is
394        // negative below the baseline (its paint path negates it before use);
395        // from_platform_metrics normalizes signs and drops unusable cap/x heights.
396        let metrics = FontRhythm::from_platform_metrics(
397            font_size.into(),
398            line_rhythms,
399            text_system.ascent(font_id, font_size).into(),
400            text_system.descent(font_id, font_size).into(),
401            text_system.cap_height(font_id, font_size).into(),
402            text_system.x_height(font_id, font_size).into(),
403        );
404
405        Self {
406            font,
407            font_id: Some(font_id),
408            metrics,
409            grid,
410        }
411    }
412
413    /// Compatibility constructor for a Plumber/rhythm-sass `baseline-ratio`.
414    /// Prefer [`Self::resolve`]; see [`FontRhythm::from_baseline_ratio`].
415    ///
416    /// # Panics
417    ///
418    /// Panics when the font weight is zero, negative, or non-finite, when
419    /// `font_size` is zero, negative, or non-finite, when `line_rhythms` is
420    /// zero, or when `baseline_ratio` is not strictly between 0 and 1.
421    pub fn from_baseline_ratio(
422        font: Font,
423        font_size: Pixels,
424        line_rhythms: u32,
425        baseline_ratio: f32,
426        grid: RhythmGrid,
427    ) -> Self {
428        assert_valid_font_request(&font, font_size, line_rhythms);
429        Self {
430            font,
431            font_id: None,
432            metrics: FontRhythm::from_baseline_ratio(
433                font_size.into(),
434                line_rhythms,
435                baseline_ratio,
436            ),
437            grid,
438        }
439    }
440
441    /// Measure an ideographic character-face anchor from this resolved font.
442    ///
443    /// The result owns a clone of this font together with the tallest ink over
444    /// `probes`, which should be full-frame glyphs of the script being set
445    /// (`"字永語国"` for han, `"곽뻠한"` for hangul). Bounds that do not
446    /// straddle the alphabetic baseline are ignored because they cannot
447    /// establish that full frame. Only the returned [`RhythmIcfAnchor`] exposes
448    /// the infallible [`RhythmIcfAnchor::span`] and
449    /// [`RhythmIcfAnchor::trim_top`] operations.
450    ///
451    /// Measuring beats reading the font's `BASE` table. `BASE` is absent from
452    /// some faces (SimSong ships none), and where present its `icft` is a
453    /// *declaration* the ink need not match: measured against their own
454    /// tables, PingFang SC and Toppan Bunkyu Gothic agree within 0.003 em, but
455    /// Apple SD Gothic Neo's hangul overshoots its declared `icft` by
456    /// 0.054 em. A measurement adapts to the face and the script; a table
457    /// lookup does not.
458    ///
459    /// Like a cap anchor, this anchors *one* ink envelope: glyphs that reach
460    /// it land on the grid line and shorter ones sit below, exactly as Latin
461    /// lowercase sits below a `cap_top` anchor. Kana are the case to know
462    /// about — their dakuten ride above the han envelope by up to 0.058 em on
463    /// the faces measured here, like Latin ascenders above cap height, though
464    /// the overshoot belongs to the face and some versions show none — so
465    /// include kana in `probes` when setting Japanese that must not exceed the
466    /// line.
467    ///
468    /// Measurement happens only at this boundary; spacing on a successful
469    /// anchor stays pure geometry. Use the same [`TextSystem`] that resolved
470    /// this font because its [`FontId`] is local to that text system. The base
471    /// `RhythmFont` remains determined entirely by [`Self::spec`] and is safe to
472    /// cache normally. Cache a measured anchor separately only when useful,
473    /// keyed by both the spec and probes.
474    ///
475    /// Availability follows gpui's text backend. In gpui 0.2.2, CoreText and
476    /// DirectWrite expose glyph ink bounds, while Linux returns advance-only
477    /// placeholder bounds that produce [`IcfMeasurementError::NoUsableBounds`].
478    /// When every bounds query fails instead, this returns
479    /// [`IcfMeasurementError::NoProbeBounds`]. If failures and rejected bounds
480    /// are mixed, the returned-bounds distinction wins and the result is
481    /// `NoUsableBounds`. Probe with glyphs the resolved face actually covers:
482    /// CoreText and Linux report a missing glyph as absent, while DirectWrite
483    /// substitutes `.notdef`, whose box can pass for ink.
484    pub fn measure_icf(
485        &self,
486        text_system: &TextSystem,
487        probes: &str,
488    ) -> Result<RhythmIcfAnchor, IcfMeasurementError> {
489        let font_id = self.font_id.ok_or(IcfMeasurementError::UnresolvedFont)?;
490        if probes.is_empty() {
491            return Err(IcfMeasurementError::EmptyProbes);
492        }
493        let font_size = self.font_size();
494        let ascent = select_icf_ascent(
495            probes
496                .chars()
497                .map(|ch| text_system.typographic_bounds(font_id, font_size, ch)),
498        )?;
499
500        Ok(RhythmIcfAnchor {
501            font: self.clone(),
502            ascent: px(ascent),
503        })
504    }
505
506    /// The requested gpui font configuration applied by
507    /// [`RhythmStyled::rhythm_font`].
508    pub fn font(&self) -> &Font {
509        &self.font
510    }
511
512    /// The font identity gpui resolved the metrics from — the fallback
513    /// font's when the requested family was unavailable. `None` when the
514    /// value was built without a text system
515    /// ([`Self::from_baseline_ratio`]).
516    ///
517    /// A [`FontId`] is an index into the resolving `TextSystem`: compare it
518    /// with shaped-run font ids from the same system, but do not persist it
519    /// or carry it across windows or font registrations — use
520    /// [`Self::spec`] for durable request keys. "Did fallback happen" has no
521    /// precise reverse lookup: `TextSystem::get_font_for_id` returns one
522    /// cached font request for an id, not an authoritative platform-face
523    /// identity, and can be ambiguous when several requests resolve to the
524    /// same face. Check `TextSystem::all_font_names` before resolving when
525    /// using the exact family is a requirement.
526    #[inline]
527    pub const fn resolved_font_id(&self) -> Option<FontId> {
528        self.font_id
529    }
530
531    /// The TextSystem resolution request that produced this font, as a
532    /// hashable cache key. Returns `None` for values synthesized by
533    /// [`Self::from_baseline_ratio`], because their ratio-derived metrics
534    /// cannot be reproduced by [`RhythmFontSpec::resolve`].
535    pub fn spec(&self) -> Option<RhythmFontSpec> {
536        self.font_id?;
537        Some(RhythmFontSpec {
538            font: self.font.clone(),
539            font_size: self.font_size(),
540            line_rhythms: self.metrics.line_rhythms(),
541            grid_size: self.grid.size(),
542        })
543    }
544
545    /// This font's line placement as a [`RhythmLineMetrics`] — the same
546    /// value a shaped line produces, so a direct-paint renderer places
547    /// single-style text, empty lines, and mixed-run shaped lines through
548    /// one code path.
549    pub fn line_metrics(&self) -> RhythmLineMetrics {
550        self.metrics.line_metrics(self.grid.rhythm())
551    }
552
553    /// The grid this font was resolved against.
554    #[inline]
555    pub const fn grid(&self) -> RhythmGrid {
556        self.grid
557    }
558
559    /// Top spacing that lands the first baseline `n` rhythm units below the
560    /// element's padding edge (rhythm-sass `baseline-top()` / `rhythm-bottom()`).
561    ///
562    /// Negative when `n × grid size` is smaller than
563    /// [`baseline_above`](Self::baseline_above) — meaningful as a margin, not
564    /// as a padding.
565    #[inline]
566    pub fn baseline_top(&self, n: i32) -> Pixels {
567        px(self.metrics.baseline_top(self.grid.rhythm(), n))
568    }
569
570    /// Bottom spacing that puts the nth grid line below the last baseline at
571    /// the element's padding edge (rhythm-sass `baseline-bottom()` /
572    /// `rhythm-top()`).
573    ///
574    /// Negative when `n × grid size` is smaller than the baseline-to-bottom
575    /// distance — meaningful as a margin, not as a padding.
576    #[inline]
577    pub fn baseline_bottom(&self, n: i32) -> Pixels {
578        px(self.metrics.baseline_bottom(self.grid.rhythm(), n))
579    }
580
581    /// Spacing from a block set in this font down to a following block set in
582    /// `below`, so the two adjacent baselines are exactly `n` rhythm units
583    /// apart (rhythm-sass `baseline-between()`).
584    ///
585    /// gpui's flex layout never collapses margins, so apply the result to
586    /// exactly one side (or as a `gap`), unlike the CSS original. Negative
587    /// results overlap the blocks when applied.
588    ///
589    /// # Panics
590    ///
591    /// Panics when `below` was resolved against a different grid size; its
592    /// line height would no longer match the calculated spacing.
593    #[inline]
594    pub fn baseline_between(&self, below: &RhythmFont, n: i32) -> Pixels {
595        assert_eq!(
596            self.grid, below.grid,
597            "both fonts must be resolved against the same grid size"
598        );
599        px(self
600            .metrics
601            .baseline_between(self.grid.rhythm(), &below.metrics, n))
602    }
603
604    /// Top spacing that lands the capitals' ink top — not the baseline — on
605    /// the nth grid line, for optically-aligned openings. Close the block
606    /// with [`Self::cap_bottom`], not [`Self::baseline_bottom`]; see
607    /// [`FontRhythm::cap_top`] for the contract, or use [`Self::cap_span`] to get
608    /// the pair in one call. `None` when the font has no usable cap height.
609    ///
610    /// CJK faces usually resolve with a cap height — for their embedded Latin
611    /// glyphs — so on ideographic text this returns `Some` while trimming to
612    /// the wrong ink; use [`Self::measure_icf`] for CJK openings.
613    #[inline]
614    pub fn cap_top(&self, n: i32) -> Option<Pixels> {
615        self.metrics.cap_top(self.grid.rhythm(), n).map(px)
616    }
617
618    /// Bottom spacing pairing [`Self::cap_top`], returning the trimmed space
619    /// so the block closes on whole rhythm rows; see [`FontRhythm::cap_bottom`].
620    /// `None` when the font has no usable cap height.
621    #[inline]
622    pub fn cap_bottom(&self, n: i32) -> Option<Pixels> {
623        self.metrics.cap_bottom(self.grid.rhythm(), n).map(px)
624    }
625
626    /// The cap-anchored opening as one paired value:
627    /// `(cap_top(top), cap_bottom(bottom))`. Taking the pair from a single
628    /// call keeps the matching anchors together and reduces the chance of
629    /// closing a cap opening with [`Self::baseline_bottom`] by mistake.
630    ///
631    /// `None` when the font has no usable cap height. The baseline fallback
632    /// is a design choice (the equivalent baseline count differs from `top`
633    /// by the cap height), so pick it explicitly, e.g. with gpui's `.map()`:
634    ///
635    /// ```no_run
636    /// # use gpui::{div, prelude::*};
637    /// # use rhythm_gpui::RhythmFont;
638    /// # fn opening(heading: &RhythmFont) -> impl IntoElement {
639    /// div().map(|d| match heading.cap_span(4, 0) {
640    ///     Some((pt, pb)) => d.pt(pt).pb(pb),
641    ///     None => d.pt(heading.baseline_top(7)),
642    /// })
643    /// # }
644    /// ```
645    #[inline]
646    pub fn cap_span(&self, top: i32, bottom: i32) -> Option<(Pixels, Pixels)> {
647        Some((self.cap_top(top)?, self.cap_bottom(bottom)?))
648    }
649
650    /// Resolve `font` as a drop cap sunk `lines` lines deep into text set in
651    /// this font — [`RhythmDropCap::resolve`] with the body slot filled in;
652    /// see it for the solving contract.
653    ///
654    /// # Panics
655    ///
656    /// Panics when `lines` is zero or `lines × line_rhythms` overflows `u32`.
657    pub fn drop_cap(&self, text_system: &TextSystem, font: Font, lines: u32) -> RhythmDropCap {
658        RhythmDropCap::resolve(text_system, font, self, lines)
659    }
660
661    /// Resolved vertical metrics in logical pixels.
662    ///
663    /// These belong to the fallback font when gpui could not load the requested
664    /// family; see [`Self::resolve`].
665    #[inline]
666    pub const fn metrics(&self) -> &FontRhythm {
667        &self.metrics
668    }
669
670    /// The font size the metrics were resolved at.
671    #[inline]
672    pub fn font_size(&self) -> Pixels {
673        px(self.metrics.font_size())
674    }
675
676    /// The rhythm line height: `line_rhythms × grid size`.
677    #[inline]
678    pub fn line_height(&self) -> Pixels {
679        px(self.metrics.line_height(self.grid.rhythm()))
680    }
681
682    /// Distance from the top of the line box down to the baseline, as gpui will
683    /// paint it. Useful for custom elements and debug overlays.
684    #[inline]
685    pub fn baseline_above(&self) -> Pixels {
686        px(self.metrics.baseline_above(self.grid.rhythm()))
687    }
688
689    /// Distance from the baseline down to the bottom of the line box, as gpui
690    /// will paint it — the counterpart of [`Self::baseline_above`].
691    #[inline]
692    pub fn baseline_below(&self) -> Pixels {
693        px(self.metrics.baseline_below(self.grid.rhythm()))
694    }
695
696    /// Invisible space above the cap height; subtract from a top spacing (or apply
697    /// as a negative margin) for CSS `text-box-trim`-style optical alignment.
698    /// `None` when the metrics source has no usable cap height, including values
699    /// created with [`Self::from_baseline_ratio`].
700    #[inline]
701    pub fn cap_trim_top(&self) -> Option<Pixels> {
702        self.metrics.cap_trim_top(self.grid.rhythm()).map(px)
703    }
704
705    /// Like [`Self::cap_trim_top`] but trimming to the x-height. `None` when the
706    /// metrics source has no usable x-height.
707    #[inline]
708    pub fn x_trim_top(&self) -> Option<Pixels> {
709        self.metrics.x_trim_top(self.grid.rhythm()).map(px)
710    }
711}
712
713/// The pre-resolve identity of a [`RhythmFont`]: the requested [`Font`],
714/// size, line rhythms, and grid size as one hashable value — the cache key
715/// for caller-owned typography catalogs.
716///
717/// The crate deliberately keeps no font cache (gpui already caches
718/// `Font → FontId` and metrics); an app reusing resolved values across a
719/// document owns the map and its invalidation. Register font families before
720/// resolving any spec for them: gpui caches failed lookups, so clearing this
721/// caller-owned map after late registration cannot repair an earlier miss.
722/// Rebuild the map when typography settings change:
723///
724/// ```no_run
725/// use std::collections::HashMap;
726///
727/// use gpui::{font, px, TextSystem};
728/// use rhythm_gpui::{RhythmFont, RhythmFontSpec, RhythmGrid};
729///
730/// fn body(cache: &mut HashMap<RhythmFontSpec, RhythmFont>, ts: &TextSystem) -> RhythmFont {
731///     let spec = RhythmFontSpec::new(font("Noto Serif"), px(16.), 3, RhythmGrid::new(px(8.)));
732///     cache
733///         .entry(spec.clone())
734///         .or_insert_with(|| spec.resolve(ts))
735///         .clone()
736/// }
737/// ```
738#[derive(Debug, Clone, PartialEq, Eq, Hash)]
739pub struct RhythmFontSpec {
740    font: Font,
741    font_size: Pixels,
742    line_rhythms: u32,
743    grid_size: Pixels,
744}
745
746impl RhythmFontSpec {
747    /// The spec for resolving `font` at `font_size` on `grid` with a
748    /// `line_rhythms`-unit line height.
749    ///
750    /// # Panics
751    ///
752    /// Panics when the font weight is zero, negative, or non-finite, when
753    /// `font_size` is zero, negative, or non-finite, or when `line_rhythms` is
754    /// zero.
755    pub fn new(font: Font, font_size: Pixels, line_rhythms: u32, grid: RhythmGrid) -> Self {
756        assert_valid_font_request(&font, font_size, line_rhythms);
757        Self {
758            font,
759            font_size,
760            line_rhythms,
761            grid_size: grid.size(),
762        }
763    }
764
765    /// The requested gpui font configuration.
766    pub fn font(&self) -> &Font {
767        &self.font
768    }
769
770    /// The font size the metrics will be resolved at.
771    #[inline]
772    pub const fn font_size(&self) -> Pixels {
773        self.font_size
774    }
775
776    /// Line height in whole rhythm units.
777    #[inline]
778    pub const fn line_rhythms(&self) -> u32 {
779        self.line_rhythms
780    }
781
782    /// The grid the font will be bound to.
783    #[inline]
784    pub fn grid(&self) -> RhythmGrid {
785        RhythmGrid::new(self.grid_size)
786    }
787
788    /// Resolve the spec into a [`RhythmFont`] — [`RhythmFont::resolve`] with
789    /// this identity; see it for the fallback-resolution caveats.
790    pub fn resolve(&self, text_system: &TextSystem) -> RhythmFont {
791        RhythmFont::resolve(
792            text_system,
793            self.font.clone(),
794            self.font_size,
795            self.line_rhythms,
796            self.grid(),
797        )
798    }
799
800    /// Resolve this spec at a line height covering the ascent/descent envelope
801    /// of every font in `others` — the catalog-build step that fixes one row
802    /// budget over an explicit face set rather than per shaped line.
803    ///
804    /// A line shapes to the maxima over its explicit font runs, so a style
805    /// needs a line height covering the tallest mixture of every face the
806    /// caller knows its runs can explicitly select: bold, inline code, or an
807    /// explicit CJK or emoji face. `others` is that closed, caller-supplied
808    /// catalog. Each listed [`Font`] is resolved through gpui — including the
809    /// family fallback gpui chooses when that request is missing — but this
810    /// method neither inspects text nor discovers glyph-level fallback faces
811    /// selected later by the platform shaper.
812    ///
813    /// gpui shapes all `TextRun`s in a line at one font size, so every spec must
814    /// use this spec's `font_size` and grid. Compatibility is checked before any
815    /// font is resolved, then the resolved metrics are folded with
816    /// [`RhythmLineMetrics::covering`]. The result is *this* spec's font at the
817    /// covering count — metrics, cap height, and baselines stay the primary
818    /// face's, only the line height grows. Nothing is shaped, so the count is a
819    /// startup constant and every block's height follows from its line count,
820    /// which is what a virtualized renderer needs.
821    ///
822    /// Keep placing each shaped line with its own `ascent`/`descent` at this
823    /// font's [`line_rhythms`](FontRhythm::line_rhythms); with the height
824    /// settled here, no explicit-run mixture of the covered set outgrows it.
825    /// Resolve the run faces at that same count when their own metrics
826    /// are used for placement — [`spec`](RhythmFont::spec) on the returned
827    /// font carries it, and reproduces these metrics as usual.
828    ///
829    /// ```no_run
830    /// use gpui::{font, px, FontWeight, TextSystem};
831    /// use rhythm_gpui::{RhythmFontSpec, RhythmGrid};
832    ///
833    /// fn body(text_system: &TextSystem) -> rhythm_gpui::RhythmFont {
834    ///     let grid = RhythmGrid::new(px(8.));
835    ///     let mut bold = font("Georgia");
836    ///     bold.weight = FontWeight::BOLD;
837    ///     RhythmFontSpec::new(font("Georgia"), px(16.), 3, grid).resolve_covering(
838    ///         text_system,
839    ///         &[
840    ///             RhythmFontSpec::new(bold, px(16.), 3, grid),
841    ///             RhythmFontSpec::new(font("Menlo"), px(16.), 3, grid),
842    ///             RhythmFontSpec::new(font("Apple Color Emoji"), px(16.), 3, grid),
843    ///         ],
844    ///     )
845    /// }
846    /// ```
847    ///
848    /// # Panics
849    ///
850    /// Panics when a spec in `others` was built with a different font size or
851    /// grid size.
852    pub fn resolve_covering(
853        &self,
854        text_system: &TextSystem,
855        others: &[RhythmFontSpec],
856    ) -> RhythmFont {
857        for spec in others {
858            assert_eq!(
859                spec.grid_size, self.grid_size,
860                "every covering font spec must use the primary grid size"
861            );
862            assert_eq!(
863                spec.font_size, self.font_size,
864                "every covering font spec must use the primary font size"
865            );
866        }
867
868        let primary = self.resolve(text_system);
869        let mut covered = Vec::with_capacity(others.len() + 1);
870        covered.push(primary.line_metrics());
871        covered.extend(
872            others
873                .iter()
874                .map(|spec| spec.resolve(text_system).line_metrics()),
875        );
876        let line_rhythms =
877            RhythmLineMetrics::covering(&covered, self.grid().rhythm()).line_rhythms();
878
879        if line_rhythms == self.line_rhythms {
880            return primary;
881        }
882        Self {
883            line_rhythms,
884            ..self.clone()
885        }
886        .resolve(text_system)
887    }
888}
889
890/// A drop cap bound to the grid: the cap face at the size solved by
891/// [`FontRhythm::drop_cap`], plus the inset anchoring its baseline. Apply with
892/// [`RhythmStyled::rhythm_drop_cap`]; for wrap-around text, measure the letter
893/// with `shape_line` (see `drop_cap_paragraph` in the `recipes` example).
894///
895/// # Examples
896///
897/// ```no_run
898/// use gpui::{div, font, prelude::*, px, FontWeight, TextSystem};
899/// use rhythm_gpui::{RhythmDropCap, RhythmFont, RhythmGrid, RhythmStyled};
900///
901/// fn drop_cap_block(text_system: &TextSystem) -> impl IntoElement {
902///     let grid = RhythmGrid::new(px(8.));
903///     let body = RhythmFont::resolve(text_system, font("Georgia"), px(16.), 3, grid);
904///     let mut bold = font("Georgia");
905///     bold.weight = FontWeight::BOLD;
906///     let cap = RhythmDropCap::resolve(text_system, bold, &body, 3);
907///
908///     div()
909///         .flex()
910///         .items_start()
911///         .gap(px(12.))
912///         .child(div().rhythm_drop_cap(&cap).child("W"))
913///         .child(div().flex_1().min_w_0().rhythm_font(&body).child("hen…"))
914/// }
915/// ```
916#[derive(Debug, Clone)]
917pub struct RhythmDropCap {
918    font: RhythmFont,
919    top: Pixels,
920}
921
922impl RhythmDropCap {
923    /// Resolve `font` as a drop cap sunk `lines` lines deep into `body` text.
924    ///
925    /// The solved size spans the capital from the first line's cap top down to
926    /// the `lines`-th baseline. The baseline anchor is exact even when a
927    /// missing cap height falls back to the 0.7 em approximation; the fallback
928    /// only affects the visual top. See [`FontRhythm::drop_cap`] for the math.
929    ///
930    /// # Panics
931    ///
932    /// Panics when `lines` is zero or `lines × body.metrics().line_rhythms()`
933    /// overflows `u32`.
934    pub fn resolve(text_system: &TextSystem, font: Font, body: &RhythmFont, lines: u32) -> Self {
935        // The probe's line box is irrelevant: drop_cap reads only metric ratios.
936        let probe = RhythmFont::resolve(text_system, font.clone(), body.font_size(), 1, body.grid);
937        let solved = body
938            .metrics()
939            .drop_cap(body.grid.rhythm(), probe.metrics(), lines);
940        Self {
941            font: RhythmFont {
942                font,
943                // resolve_font identifies the face, not the size, so the
944                // probe's identity holds at the solved size.
945                font_id: probe.font_id,
946                metrics: *solved.metrics(),
947                grid: body.grid,
948            },
949            top: px(solved.top()),
950        }
951    }
952
953    /// The cap face at the solved size; its line box spans the sunk lines.
954    pub const fn font(&self) -> &RhythmFont {
955        &self.font
956    }
957
958    /// Relative `top` inset landing the cap's baseline on the last sunk line's
959    /// baseline. An inset rather than a margin on purpose: cap-heavy faces
960    /// (cap height exceeding `ascent − descent`, e.g. Merriweather) need a
961    /// downward shift, and a positive margin would grow the flex row's cross
962    /// size and push everything below off the grid.
963    #[inline]
964    pub const fn top(&self) -> Pixels {
965        self.top
966    }
967}
968
969/// Extension methods for applying rhythm fonts through gpui's fluent style API.
970pub trait RhythmStyled: Styled + Sized {
971    /// Apply the complete font configuration, size, and rhythm line height.
972    fn rhythm_font(self, font: &RhythmFont) -> Self {
973        self.font(font.font().clone())
974            .text_size(font.font_size())
975            .line_height(font.line_height())
976    }
977
978    /// The whole text-block recipe in one call: [`Self::rhythm_font`] plus
979    /// the paired paddings that open `top` rhythm units above the first
980    /// baseline and close `bottom` units below the last, so the block
981    /// occupies a whole number of rhythm rows for any number of wrapped
982    /// lines and composes freely without breaking the page rhythm.
983    ///
984    /// Paddings go negative when `top`/`bottom` are smaller than the font's
985    /// baseline distances; negative spacing is meaningful as a margin, so
986    /// use [`RhythmFont::baseline_top`] / [`RhythmFont::baseline_bottom`]
987    /// directly for margin-based layouts.
988    ///
989    /// # Examples
990    ///
991    /// ```no_run
992    /// use gpui::{div, font, prelude::*, px, TextSystem};
993    /// use rhythm_gpui::{RhythmGrid, RhythmStyled};
994    ///
995    /// fn card(text_system: &TextSystem) -> impl IntoElement {
996    ///     let grid = RhythmGrid::new(px(8.));
997    ///     let body = grid.font(text_system, font("Georgia"), px(16.), 3);
998    ///     div()
999    ///         .rhythm_block(&body, 3, 1)
1000    ///         .child("A block spanning whole rhythm rows.")
1001    /// }
1002    /// ```
1003    fn rhythm_block(self, font: &RhythmFont, top: i32, bottom: i32) -> Self {
1004        self.rhythm_font(font)
1005            .pt(font.baseline_top(top))
1006            .pb(font.baseline_bottom(bottom))
1007    }
1008
1009    /// Apply a drop cap: the solved font plus its baseline-anchoring relative
1010    /// `top` inset. See [`RhythmDropCap::top`] for why the anchor must not be
1011    /// applied as a margin.
1012    fn rhythm_drop_cap(self, cap: &RhythmDropCap) -> Self {
1013        self.rhythm_font(cap.font()).relative().top(cap.top())
1014    }
1015
1016    /// Paint the debug grid over this element while `show` is true. Pass the
1017    /// grid itself for the classic translucent red (`0xff78783f`), or
1018    /// [`grid.overlay(color)`](RhythmGrid::overlay) — optionally with
1019    /// [`phase`](RhythmOverlay::phase) — to customize the stripes through the
1020    /// same chainable toggle. Chain it after the content children so the
1021    /// stripes paint on top; the element's top edge becomes the grid origin,
1022    /// and the element's own position style is left untouched.
1023    ///
1024    /// # Examples
1025    ///
1026    /// ```no_run
1027    /// use gpui::{div, prelude::*, px, rgba};
1028    /// use rhythm_gpui::{RhythmGrid, RhythmStyled};
1029    ///
1030    /// fn page(show_grid: bool) -> impl IntoElement {
1031    ///     let grid = RhythmGrid::new(px(8.));
1032    ///     div()
1033    ///         .child("…content on the grid…")
1034    ///         .rhythm_debug_overlay(grid, show_grid)
1035    /// }
1036    ///
1037    /// fn tinted_page(show_grid: bool) -> impl IntoElement {
1038    ///     let grid = RhythmGrid::new(px(8.));
1039    ///     div()
1040    ///         .child("…content on the grid…")
1041    ///         .rhythm_debug_overlay(grid.overlay(rgba(0x0969da33)), show_grid)
1042    /// }
1043    /// ```
1044    fn rhythm_debug_overlay(self, overlay: impl Into<RhythmOverlay>, show: bool) -> Self
1045    where
1046        Self: ParentElement,
1047    {
1048        if show {
1049            self.child(overlay.into())
1050        } else {
1051            self
1052        }
1053    }
1054}
1055
1056impl<T: Styled> RhythmStyled for T {}
1057
1058/// Create a [`RhythmOverlay`] painting every other grid row in `color` — the
1059/// rhythm-sass `draw-rhythms()` mixin. Add [`RhythmOverlay::phase`] for a
1060/// renderer that scrolls by painting at an offset.
1061pub fn rhythm_overlay(grid: RhythmGrid, color: impl Into<Hsla>) -> RhythmOverlay {
1062    RhythmOverlay {
1063        grid,
1064        color: color.into(),
1065        phase: px(0.),
1066    }
1067}
1068
1069/// A `draw-rhythms` debug overlay: every other grid row in one color.
1070/// Place it as the last child of the container it should cover; it fills that
1071/// container and ignores mouse events. An ordinary gpui container already uses
1072/// relative positioning by default, so no extra `.relative()` call is needed,
1073/// and this element does not alter the container's position style.
1074///
1075/// The stripes start at the container's top edge. If the content scrolls a
1076/// container, put the overlay *inside* the scrolled wrapper so the grid moves
1077/// with the text; a renderer that scrolls by painting content at a computed
1078/// signed Y offset instead passes that same offset to [`phase`](Self::phase).
1079/// Either way only rows intersecting the visible region (the current content
1080/// mask) are painted, and each is clipped to the overlay's own bounds, so
1081/// covering a document far taller than the viewport costs
1082/// `O(viewport height / grid size)` per frame and never paints outside the
1083/// container it covers. Custom elements whose offset settles during prepaint
1084/// can call [`paint`](Self::paint) with a freshly phased value during their
1085/// paint stage instead of capturing stale state during render.
1086#[derive(IntoElement)]
1087pub struct RhythmOverlay {
1088    grid: RhythmGrid,
1089    color: Hsla,
1090    phase: Pixels,
1091}
1092
1093impl RhythmOverlay {
1094    /// Translate the stripes by the same signed Y `offset` used to paint the
1095    /// content, for renderers that scroll without moving a scroll container.
1096    /// A negative offset moves both content and grid origin above the
1097    /// element; a positive offset moves them below it. In particular,
1098    /// `ScrollHandle::offset().y` can be passed through directly.
1099    ///
1100    /// This has the effect of translating the overlay with the content —
1101    /// without a wrapper element, and without an ancestor to clip it, since
1102    /// stripes are clipped to the overlay's own bounds.
1103    ///
1104    /// # Panics
1105    ///
1106    /// Panics when `offset` is not finite.
1107    #[must_use]
1108    pub fn phase(mut self, offset: Pixels) -> Self {
1109        assert!(
1110            f32::from(offset).is_finite(),
1111            "overlay phase must be finite"
1112        );
1113        self.phase = offset;
1114        self
1115    }
1116
1117    /// Paint the configured stripes directly into `window` within `bounds`.
1118    ///
1119    /// This is the paint-stage counterpart to using the overlay as an element.
1120    /// It is intended for custom renderers whose signed content translation is
1121    /// not final until another element's prepaint has settled layout. Read that
1122    /// application-owned value during paint, apply it to a temporary overlay
1123    /// with [`phase`](Self::phase), then call this method; the crate stores no
1124    /// callback or mutable scroll state.
1125    ///
1126    /// `bounds` takes the covered container's role, which the element form
1127    /// fills in from the parent: stripe row 0 starts at `bounds.origin.y` plus
1128    /// the [`phase`](Self::phase), and nothing paints outside the box. The
1129    /// current content mask is applied exactly as for the element form, and
1130    /// only visible stripes are visited. Call this after painting the content
1131    /// the stripes should cover.
1132    ///
1133    /// ```no_run
1134    /// use gpui::{Bounds, Pixels, Window, px, rgba};
1135    /// use rhythm_gpui::RhythmGrid;
1136    ///
1137    /// fn paint_grid(
1138    ///     bounds: Bounds<Pixels>,
1139    ///     content_offset_y: Pixels,
1140    ///     window: &mut Window,
1141    /// ) {
1142    ///     RhythmGrid::new(px(8.))
1143    ///         .overlay(rgba(0xff78783f))
1144    ///         .phase(content_offset_y)
1145    ///         .paint(bounds, window);
1146    /// }
1147    /// ```
1148    pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
1149        let visible = bounds.intersect(&window.content_mask().bounds);
1150        if visible.size.height <= px(0.) || visible.size.width <= px(0.) {
1151            return;
1152        }
1153
1154        // Apply the same signed translation as the painted content so the grid
1155        // origin stays attached to the document.
1156        let origin_y = overlay_origin_y(bounds.origin.y, self.phase);
1157        visible_stripes(
1158            origin_y,
1159            f32::from(self.grid.size()),
1160            f32::from(visible.origin.y),
1161            f32::from(visible.bottom()),
1162            |from, to| {
1163                window.paint_quad(fill(
1164                    Bounds::new(
1165                        point(bounds.origin.x, px(from)),
1166                        size(bounds.size.width, px(to - from)),
1167                    ),
1168                    self.color,
1169                ));
1170            },
1171        );
1172    }
1173}
1174
1175#[inline]
1176fn overlay_origin_y(element_top: Pixels, phase: Pixels) -> f32 {
1177    f32::from(element_top) + f32::from(phase)
1178}
1179
1180/// A bare grid converts to its default debug appearance — the classic
1181/// translucent red (`0xff78783f`), zero phase — which is what lets
1182/// [`RhythmStyled::rhythm_debug_overlay`] accept the grid directly.
1183impl From<RhythmGrid> for RhythmOverlay {
1184    fn from(grid: RhythmGrid) -> Self {
1185        rhythm_overlay(grid, rgba(DEFAULT_RHYTHM_OVERLAY_RGBA))
1186    }
1187}
1188
1189impl RenderOnce for RhythmOverlay {
1190    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
1191        canvas(
1192            |_, _, _| (),
1193            move |bounds, _, window, _| self.paint(bounds, window),
1194        )
1195        .absolute()
1196        .inset_0()
1197    }
1198}
1199
1200/// The overlay's whole geometry: every other row of the grid rooted at
1201/// `origin_y` — including any signed content translation — reported to
1202/// `paint` as `(from, to)` spans clipped to the visible `[top, bottom)`.
1203///
1204/// Whole periods before the visible region are skipped arithmetically rather
1205/// than walked, so an overlay over a document far taller than the viewport
1206/// still costs `O(viewport height / stripe height)`, and the clipping keeps
1207/// a translated first row (or an overhanging last one) inside the element.
1208fn visible_stripes(
1209    origin_y: f32,
1210    stripe_height: f32,
1211    top: f32,
1212    bottom: f32,
1213    mut paint: impl FnMut(f32, f32),
1214) {
1215    let step = stripe_height * 2.0;
1216    let skipped = (((top - origin_y - stripe_height) / step).ceil()).max(0.0);
1217    let mut y = origin_y + step * skipped;
1218    while y < bottom {
1219        let from = y.max(top);
1220        let to = (y + stripe_height).min(bottom);
1221        if to > from {
1222            paint(from, to);
1223        }
1224        let next = y + step;
1225        if !next.is_finite() || next <= y {
1226            break;
1227        }
1228        y = next;
1229    }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    use super::*;
1235    use gpui::{
1236        font, AnyElement, FontFallbacks, FontFeatures, FontId, FontStyle, FontWeight, Position,
1237        StyleRefinement,
1238    };
1239
1240    #[derive(Default)]
1241    struct CapturedStyle {
1242        style: StyleRefinement,
1243    }
1244
1245    impl Styled for CapturedStyle {
1246        fn style(&mut self) -> &mut StyleRefinement {
1247            &mut self.style
1248        }
1249    }
1250
1251    #[test]
1252    fn pixels_mirrors_agree_with_the_math_layer() {
1253        let grid = RhythmGrid::new(px(8.0));
1254        assert_eq!(grid.rhythm(), Rhythm::new(8.0));
1255        assert_eq!(grid.spacing(5), px(40.0));
1256        assert_eq!(grid.height(5), grid.spacing(5));
1257        let line = grid.line_metrics(px(14.67), px(3.51), 3);
1258        assert_eq!(line.line_height_px(), px(line.line_height()));
1259
1260        let target = grid.height(5);
1261        assert_eq!(
1262            line.paint_origin_for_px(target),
1263            px(line.paint_origin_for(target.into()))
1264        );
1265
1266        let block = RhythmBlockMetrics::new(line, 3, 1);
1267        assert_eq!(block.first_baseline_px(), px(block.first_baseline()));
1268        let wide_row = i64::from(i32::MAX) * 4;
1269        assert_eq!(
1270            block.baseline_at_row_px(wide_row),
1271            px(block.baseline_at_row(wide_row))
1272        );
1273    }
1274
1275    #[test]
1276    fn line_metrics_grid_helpers_match_the_math_layer() {
1277        let grid = RhythmGrid::new(px(8.0));
1278        let grown = grid.line_metrics_at_least(px(20.0), px(6.0), 3);
1279        assert_eq!(grown.line_rhythms(), 4);
1280        assert!(!grown.overflows_line_box());
1281
1282        let body = grid.line_metrics(px(14.67), px(3.51), 3);
1283        let display = grid.line_metrics(px(28.8), px(6.4), 3);
1284        assert_eq!(
1285            grid.line_metrics_covering(&[body, display]),
1286            RhythmLineMetrics::covering(&[body, display], body.grid())
1287        );
1288    }
1289
1290    #[test]
1291    fn spacing_methods_agree_with_the_math_layer() {
1292        // Each assertion names the math method the gpui form must reach, so a
1293        // forwarder wired to its sibling (top to bottom, cap to baseline)
1294        // fails here even though every value stays self-consistent.
1295        let grid = RhythmGrid::new(px(8.0));
1296        let rhythm = grid.rhythm();
1297        let metrics = FontRhythm::from_platform_metrics(16.0, 3, 14.67, -3.51, 11.09, 7.70);
1298        let body = RhythmFont {
1299            font: font("Example Serif"),
1300            font_id: None,
1301            metrics,
1302            grid,
1303        };
1304        let below = RhythmFont::from_baseline_ratio(font("Example Serif"), px(24.0), 4, 0.2, grid);
1305
1306        assert_eq!(body.baseline_top(3), px(metrics.baseline_top(rhythm, 3)));
1307        assert_eq!(
1308            body.baseline_bottom(1),
1309            px(metrics.baseline_bottom(rhythm, 1))
1310        );
1311        assert_eq!(
1312            body.baseline_between(&below, 6),
1313            px(metrics.baseline_between(rhythm, below.metrics(), 6))
1314        );
1315        assert_eq!(body.cap_top(3), metrics.cap_top(rhythm, 3).map(px));
1316        assert_eq!(body.cap_bottom(0), metrics.cap_bottom(rhythm, 0).map(px));
1317    }
1318
1319    #[test]
1320    fn rhythm_font_applies_the_resolved_font_contract() {
1321        let mut resolved_font = font("Example Serif");
1322        resolved_font.features = FontFeatures::disable_ligatures();
1323        resolved_font.fallbacks = Some(FontFallbacks::from_fonts(vec!["Fallback Serif".into()]));
1324        resolved_font.style = FontStyle::Oblique;
1325        let expected = resolved_font.clone();
1326        let rhythm_font = RhythmFont::from_baseline_ratio(
1327            resolved_font,
1328            px(16.0),
1329            3,
1330            0.2,
1331            RhythmGrid::new(px(8.0)),
1332        );
1333
1334        let captured = CapturedStyle::default().rhythm_font(&rhythm_font);
1335        let text = captured
1336            .style
1337            .text
1338            .expect("rhythm font should set text style");
1339        assert_eq!(text.font_family, Some(expected.family));
1340        assert_eq!(text.font_features, Some(expected.features));
1341        assert_eq!(text.font_fallbacks, expected.fallbacks);
1342        assert_eq!(text.font_weight, Some(expected.weight));
1343        assert_eq!(text.font_style, Some(expected.style));
1344    }
1345
1346    #[derive(Default)]
1347    struct CapturedChildren {
1348        style: StyleRefinement,
1349        children: Vec<AnyElement>,
1350    }
1351
1352    impl Styled for CapturedChildren {
1353        fn style(&mut self) -> &mut StyleRefinement {
1354            &mut self.style
1355        }
1356    }
1357
1358    impl ParentElement for CapturedChildren {
1359        fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1360            self.children.extend(elements)
1361        }
1362    }
1363
1364    #[test]
1365    fn rhythm_debug_overlay_appends_only_when_shown() {
1366        let grid = RhythmGrid::new(px(8.0));
1367        let hidden = CapturedChildren::default().rhythm_debug_overlay(grid, false);
1368        assert!(hidden.children.is_empty());
1369        let shown = CapturedChildren::default().rhythm_debug_overlay(grid, true);
1370        assert_eq!(shown.children.len(), 1);
1371
1372        let color = rgba(0x0969da33);
1373        let hidden = CapturedChildren::default().rhythm_debug_overlay(grid.overlay(color), false);
1374        assert!(hidden.children.is_empty());
1375        let shown = CapturedChildren::default().rhythm_debug_overlay(grid.overlay(color), true);
1376        assert_eq!(shown.children.len(), 1);
1377    }
1378
1379    #[test]
1380    fn rhythm_overlay_keeps_the_requested_grid_and_color() {
1381        let grid = RhythmGrid::new(px(10.0));
1382        let color: Hsla = rgba(0x0969da33).into();
1383        let overlay = rhythm_overlay(grid, color);
1384        assert_eq!(overlay.grid, grid);
1385        assert_eq!(overlay.color, color);
1386        let mut spans = Vec::new();
1387        visible_stripes(
1388            0.0,
1389            f32::from(overlay.grid.size()),
1390            0.0,
1391            45.0,
1392            |from, to| spans.push((from, to)),
1393        );
1394        assert_eq!(spans, [(0.0, 10.0), (20.0, 30.0), (40.0, 45.0)]);
1395        let factory = grid.overlay(color);
1396        assert_eq!(factory.grid, grid);
1397        assert_eq!(factory.color, color);
1398        assert_eq!(factory.phase, px(0.));
1399    }
1400
1401    #[test]
1402    fn a_bare_grid_converts_to_the_default_red_overlay() {
1403        let overlay = RhythmOverlay::from(RhythmGrid::new(px(8.0)));
1404        assert_eq!(overlay.color, rgba(DEFAULT_RHYTHM_OVERLAY_RGBA).into());
1405        assert_eq!(overlay.phase, px(0.));
1406    }
1407
1408    #[test]
1409    fn rhythm_drop_cap_anchors_with_a_relative_inset_not_a_margin() {
1410        let grid = RhythmGrid::new(px(8.0));
1411        let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
1412        let solved = body.metrics().drop_cap(Rhythm::new(8.0), body.metrics(), 3);
1413        let cap = RhythmDropCap {
1414            font: RhythmFont {
1415                font: font("Example Serif"),
1416                font_id: None,
1417                metrics: *solved.metrics(),
1418                grid,
1419            },
1420            top: px(solved.top()),
1421        };
1422
1423        let captured = CapturedStyle::default().rhythm_drop_cap(&cap);
1424        assert_eq!(captured.style.position, Some(Position::Relative));
1425        assert_eq!(captured.style.inset.top, Some(cap.top().into()));
1426        assert_eq!(captured.style.margin.top, None);
1427        let text = captured.style.text.expect("drop cap should set text style");
1428        assert_eq!(text.font_family, Some("Example Serif".into()));
1429    }
1430
1431    #[test]
1432    #[should_panic(expected = "same grid size")]
1433    fn baseline_between_rejects_fonts_on_different_grids() {
1434        let above = RhythmFont::from_baseline_ratio(
1435            font("Example Serif"),
1436            px(16.0),
1437            3,
1438            0.2,
1439            RhythmGrid::new(px(8.0)),
1440        );
1441        let below = RhythmFont::from_baseline_ratio(
1442            font("Example Serif"),
1443            px(16.0),
1444            3,
1445            0.2,
1446            RhythmGrid::new(px(10.0)),
1447        );
1448
1449        above.baseline_between(&below, 3);
1450    }
1451
1452    #[test]
1453    fn rhythm_block_applies_the_font_and_the_paired_paddings() {
1454        let grid = RhythmGrid::new(px(8.0));
1455        let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
1456
1457        let captured = CapturedStyle::default().rhythm_block(&body, 3, 1);
1458        assert_eq!(
1459            captured.style.padding.top,
1460            Some(body.baseline_top(3).into())
1461        );
1462        assert_eq!(
1463            captured.style.padding.bottom,
1464            Some(body.baseline_bottom(1).into())
1465        );
1466        let text = captured
1467            .style
1468            .text
1469            .expect("rhythm block should set the text style");
1470        assert_eq!(text.font_family, Some("Example Serif".into()));
1471    }
1472
1473    #[test]
1474    fn cap_span_pairs_the_anchors_and_spans_whole_rows() {
1475        let grid = RhythmGrid::new(px(8.0));
1476        // Georgia-like metrics at 16px on a 3-unit line.
1477        let metrics = FontRhythm::from_platform_metrics(16.0, 3, 14.67, -3.51, 11.09, 7.70);
1478        let heading = RhythmFont {
1479            font: font("Example Serif"),
1480            font_id: None,
1481            metrics,
1482            grid,
1483        };
1484
1485        let (pt, pb) = heading.cap_span(3, 0).expect("cap metrics are present");
1486        assert_eq!(Some(pt), heading.cap_top(3));
1487        assert_eq!(Some(pb), heading.cap_bottom(0));
1488        let rows = f32::from(pt + heading.line_height() + pb) / 8.0;
1489        assert!((rows - rows.round()).abs() < 1e-3);
1490
1491        // `cap_span` composes the `Rhythm` anchors while the ICF anchor goes
1492        // through the block metrics; pinning both to the same value keeps the
1493        // two ink-anchor paths from drifting apart.
1494        let line = metrics.line_metrics(grid.rhythm());
1495        let block =
1496            RhythmBlockMetrics::ink_anchored(line, metrics.cap_height().expect("cap height"), 3, 0);
1497        assert!((f32::from(pt) - block.opening()).abs() < 1e-4);
1498        assert!((f32::from(pb) - block.closing()).abs() < 1e-4);
1499    }
1500
1501    #[test]
1502    fn cap_spacing_returns_none_without_cap_height() {
1503        let grid = RhythmGrid::new(px(8.0));
1504        let font = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
1505
1506        assert_eq!(font.cap_top(3), None);
1507        assert_eq!(font.cap_bottom(1), None);
1508        assert_eq!(font.cap_span(3, 1), None);
1509    }
1510
1511    /// PingFang SC at 16px on a 3-unit line — hhea 1.060/-0.340, with an OS/2
1512    /// cap of 0.860 that is a placeholder copy of `sTypoAscender` rather than
1513    /// its Latin `H` — carrying the ICF ascent that
1514    /// [`RhythmFont::measure_icf`] would have measured.
1515    const PINGFANG_ICF_16: f32 = 0.822 * 16.0;
1516
1517    fn pingfang_icf_16(grid: RhythmGrid) -> RhythmIcfAnchor {
1518        RhythmIcfAnchor {
1519            font: RhythmFont {
1520                font: font("Example CJK"),
1521                font_id: None,
1522                metrics: FontRhythm::from_platform_metrics(16.0, 3, 16.96, -5.44, 13.76, 9.6),
1523                grid,
1524            },
1525            ascent: px(PINGFANG_ICF_16),
1526        }
1527    }
1528
1529    #[test]
1530    fn icf_anchor_pairs_the_anchors_and_spans_whole_rows() {
1531        let grid = RhythmGrid::new(px(8.0));
1532        let anchor = pingfang_icf_16(grid);
1533        let body = anchor.font();
1534
1535        let (pt, pb) = anchor.span(3, 0);
1536        let trim = anchor.trim_top();
1537        assert_eq!(pt, grid.height(3) - trim);
1538        assert_eq!(pb, trim);
1539        // The character face's top edge lands on the third grid line…
1540        assert_eq!(pt + trim, grid.height(3));
1541        // …and the pair still spans whole rhythm rows.
1542        let rows = f32::from(pt + body.line_height() + pb) / 8.0;
1543        assert!((rows - rows.round()).abs() < 1e-3);
1544        // The reported cap height anchors somewhere else entirely.
1545        assert_ne!(body.cap_top(3), Some(pt));
1546    }
1547
1548    #[test]
1549    fn icf_anchor_agrees_with_the_generic_block_metrics() {
1550        // The gpui pair is a convenience over a capability the math layer
1551        // already has; if these ever disagree, one of them is wrong.
1552        let grid = RhythmGrid::new(px(8.0));
1553        let anchor = pingfang_icf_16(grid);
1554        let body = anchor.font();
1555
1556        let (pt, pb) = anchor.span(3, 0);
1557        let line = body.metrics().line_metrics(grid.rhythm());
1558        let block = RhythmBlockMetrics::ink_anchored(line, PINGFANG_ICF_16, 3, 0);
1559        assert!((f32::from(pt) - block.opening()).abs() < 1e-4);
1560        assert!((f32::from(pb) - block.closing()).abs() < 1e-4);
1561    }
1562
1563    #[test]
1564    fn icf_measurement_rejects_advance_only_placeholder_bounds() {
1565        let real_ink = Bounds {
1566            origin: point(px(0.0), px(-1.6)),
1567            size: size(px(16.0), px(14.8)),
1568        };
1569        assert!((ideographic_ink_ascent(real_ink).unwrap() - 13.2).abs() < 1e-4);
1570
1571        // gpui 0.2.2's Linux backend currently returns an advance rectangle
1572        // rooted at zero instead of glyph ink bounds. A nonzero vertical
1573        // advance must not become a plausible-looking ICF ascent.
1574        let advance_only = Bounds {
1575            origin: point(px(0.0), px(0.0)),
1576            size: size(px(16.0), px(16.0)),
1577        };
1578        assert_eq!(ideographic_ink_ascent(advance_only), None);
1579
1580        assert_eq!(
1581            select_icf_ascent([Err::<Bounds<Pixels>, ()>(())]),
1582            Err(IcfMeasurementError::NoProbeBounds)
1583        );
1584        assert_eq!(
1585            select_icf_ascent([Ok::<Bounds<Pixels>, ()>(advance_only)]),
1586            Err(IcfMeasurementError::NoUsableBounds)
1587        );
1588        assert_eq!(
1589            select_icf_ascent([Err(()), Ok(advance_only)]),
1590            Err(IcfMeasurementError::NoUsableBounds),
1591            "a returned-but-rejected bound wins over failed probe queries"
1592        );
1593        assert!(
1594            (select_icf_ascent([Err(()), Ok(real_ink)]).unwrap() - 13.2).abs() < 1e-4,
1595            "one usable probe makes the aggregate measurement succeed"
1596        );
1597    }
1598
1599    #[test]
1600    #[should_panic(expected = "rhythm unit size must be finite and greater than zero")]
1601    fn grid_rejects_a_non_positive_size() {
1602        let _ = RhythmGrid::new(px(0.0));
1603    }
1604
1605    /// The overlay's painted spans for an 8px grid rooted at `origin_y`,
1606    /// over the visible region `[top, bottom)`.
1607    fn stripe_spans(origin_y: f32, top: f32, bottom: f32) -> Vec<(f32, f32)> {
1608        let mut spans = Vec::new();
1609        visible_stripes(origin_y, 8.0, top, bottom, |from, to| {
1610            spans.push((from, to))
1611        });
1612        spans
1613    }
1614
1615    #[test]
1616    fn overlay_stripes_every_other_row_from_the_container_top() {
1617        assert_eq!(
1618            stripe_spans(0.0, 0.0, 40.0),
1619            [(0.0, 8.0), (16.0, 24.0), (32.0, 40.0)]
1620        );
1621        // The last row is clipped to the element instead of bleeding past it.
1622        assert_eq!(
1623            stripe_spans(0.0, 0.0, 36.0),
1624            [(0.0, 8.0), (16.0, 24.0), (32.0, 36.0)]
1625        );
1626    }
1627
1628    #[test]
1629    fn overlay_walks_only_the_visible_region_and_keeps_the_phase() {
1630        // A row still partially visible at the mask's top edge is kept and
1631        // clipped to it; a whole 1600px of scrolled-past rows costs no work.
1632        assert_eq!(
1633            stripe_spans(0.0, 1607.0, 1630.0),
1634            [(1607.0, 1608.0), (1616.0, 1624.0)]
1635        );
1636        // One pixel later that row is gone, and the phase still holds.
1637        assert_eq!(stripe_spans(0.0, 1609.0, 1630.0), [(1616.0, 1624.0)]);
1638    }
1639
1640    #[test]
1641    fn overlay_phase_uses_the_contents_signed_translation() {
1642        let grid = RhythmGrid::new(px(8.0));
1643
1644        // GPUI scroll offsets become negative as content moves up. Passing
1645        // that value through directly moves the overlay origin the same way:
1646        // after one row the opening stripe is gone and the next starts at 8.
1647        let one_row = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-8.0));
1648        let one_row_origin = overlay_origin_y(px(0.0), one_row.phase);
1649        assert_eq!(one_row_origin, -8.0);
1650        assert_eq!(
1651            stripe_spans(one_row_origin, 0.0, 40.0),
1652            [(8.0, 16.0), (24.0, 32.0)]
1653        );
1654
1655        // Half a row leaves the visible half of the translated stripe at the
1656        // element's top edge — no ancestor clipping needed.
1657        let half_row = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-4.0));
1658        let half_row_origin = overlay_origin_y(px(0.0), half_row.phase);
1659        assert_eq!(
1660            stripe_spans(half_row_origin, 0.0, 40.0),
1661            [(0.0, 4.0), (12.0, 20.0), (28.0, 36.0)]
1662        );
1663
1664        // A positive content translation moves the document origin down and
1665        // leaves the area before it unpainted.
1666        let down = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(4.0));
1667        let down_origin = overlay_origin_y(px(0.0), down.phase);
1668        assert_eq!(down_origin, 4.0);
1669        assert_eq!(
1670            stripe_spans(down_origin, 0.0, 24.0),
1671            [(4.0, 12.0), (20.0, 24.0)]
1672        );
1673
1674        // Deep into a scrolled document the signed phase is still exact, and
1675        // only the visible period is walked.
1676        let deep = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-1600.0));
1677        let deep_origin = overlay_origin_y(px(0.0), deep.phase);
1678        assert_eq!(
1679            stripe_spans(deep_origin, 0.0, 24.0),
1680            [(0.0, 8.0), (16.0, 24.0)]
1681        );
1682    }
1683
1684    #[test]
1685    fn overlay_stops_when_a_stride_is_below_coordinate_precision() {
1686        // Both values are valid f32 coordinates, but adding this period at
1687        // y=100 rounds back to 100. The paint loop must stop instead of
1688        // waiting forever for a representable next stripe.
1689        assert_eq!(100.0f32 + 2.0e-7, 100.0);
1690        let mut spans = Vec::new();
1691        visible_stripes(0.0, 1.0e-7, 100.0, 101.0, |from, to| spans.push((from, to)));
1692        assert!(spans.is_empty());
1693    }
1694
1695    #[test]
1696    #[should_panic(expected = "overlay phase must be finite")]
1697    fn overlay_rejects_a_non_finite_phase() {
1698        let _ = rhythm_overlay(RhythmGrid::new(px(8.0)), rgba(0xff78783f)).phase(px(f32::NAN));
1699    }
1700
1701    #[test]
1702    fn covering_reaches_the_math_layer_from_grid_built_lines() {
1703        let grid = RhythmGrid::new(px(8.0));
1704        let body = grid.line_metrics(px(14.67), px(3.51), 3);
1705        let display = grid.line_metrics(px(28.8), px(6.4), 3);
1706        let covering = grid.line_metrics_covering(&[body, display]);
1707
1708        // Three rows cannot hold the display face's metric envelope; five can,
1709        // and every mixture of the pair fits that count.
1710        assert_eq!(covering.line_rhythms(), 5);
1711        assert!(!grid
1712            .line_metrics(px(28.8), px(6.4), covering.line_rhythms())
1713            .overflows_line_box());
1714    }
1715
1716    #[test]
1717    fn spec_keys_a_map_for_text_system_resolved_fonts() {
1718        use std::collections::HashMap;
1719
1720        let grid = RhythmGrid::new(px(8.0));
1721        let metrics = FontRhythm::from_baseline_ratio(16.0, 3, 0.2);
1722        let body = RhythmFont {
1723            font: font("Example Serif"),
1724            font_id: Some(FontId(7)),
1725            metrics,
1726            grid,
1727        };
1728        let spec = body.spec().expect("font came from a text system");
1729        assert_eq!(
1730            spec,
1731            RhythmFontSpec::new(font("Example Serif"), px(16.0), 3, grid)
1732        );
1733        assert_eq!(spec.font(), body.font());
1734        assert_eq!(spec.font_size(), body.font_size());
1735        assert_eq!(spec.line_rhythms(), 3);
1736        assert_eq!(spec.grid(), grid);
1737
1738        let mut cache = HashMap::new();
1739        cache.insert(spec.clone(), body);
1740        assert!(cache.contains_key(&spec));
1741        // A different size is a different identity.
1742        let other = RhythmFontSpec::new(font("Example Serif"), px(17.0), 3, grid);
1743        assert!(!cache.contains_key(&other));
1744    }
1745
1746    #[test]
1747    fn spec_rejects_invalid_request_values_before_it_can_be_a_cache_key() {
1748        let grid = RhythmGrid::new(px(8.0));
1749        for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
1750            let result = std::panic::catch_unwind(|| {
1751                RhythmFontSpec::new(font("Example Serif"), px(invalid), 3, grid)
1752            });
1753            assert!(result.is_err(), "accepted invalid font size {invalid:?}");
1754        }
1755        let zero_lines = std::panic::catch_unwind(|| {
1756            RhythmFontSpec::new(font("Example Serif"), px(16.0), 0, grid)
1757        });
1758        assert!(zero_lines.is_err(), "accepted a zero line-rhythm count");
1759
1760        for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
1761            let mut invalid_font = font("Example Serif");
1762            invalid_font.weight = FontWeight(invalid);
1763            let result =
1764                std::panic::catch_unwind(|| RhythmFontSpec::new(invalid_font, px(16.0), 3, grid));
1765            assert!(result.is_err(), "accepted invalid font weight {invalid:?}");
1766        }
1767    }
1768
1769    #[test]
1770    fn baseline_ratio_font_rejects_an_invalid_font_weight() {
1771        let grid = RhythmGrid::new(px(8.0));
1772        for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
1773            let mut invalid_font = font("Example Serif");
1774            invalid_font.weight = FontWeight(invalid);
1775            let result = std::panic::catch_unwind(|| {
1776                RhythmFont::from_baseline_ratio(invalid_font, px(16.0), 3, 0.2, grid)
1777            });
1778            assert!(result.is_err(), "accepted invalid font weight {invalid:?}");
1779        }
1780    }
1781
1782    #[test]
1783    fn baseline_ratio_fonts_carry_no_resolved_identity() {
1784        let grid = RhythmGrid::new(px(8.0));
1785        let first = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
1786        let second = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.3, grid);
1787        assert_eq!(first.resolved_font_id(), None);
1788        assert_eq!(first.spec(), None);
1789        assert_eq!(second.spec(), None);
1790        assert_ne!(first.metrics(), second.metrics());
1791    }
1792
1793    #[test]
1794    fn line_metric_factories_agree_with_the_math_layer() {
1795        let grid = RhythmGrid::new(px(8.0));
1796        let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
1797        let from_font = body.line_metrics();
1798        let from_grid =
1799            grid.line_metrics(px(body.metrics().ascent()), px(body.metrics().descent()), 3);
1800        assert_eq!(from_font, from_grid);
1801        assert_eq!(f32::from(body.baseline_above()), from_font.baseline_above());
1802        assert_eq!(f32::from(body.baseline_below()), from_font.baseline_below());
1803        assert_eq!(f32::from(body.line_height()), from_font.line_height());
1804    }
1805}