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}