use gpui::{
canvas, fill, point, px, rgba, size, App, Bounds, Font, FontId, Hsla, IntoElement,
ParentElement, Pixels, RenderOnce, Styled, TextSystem, Window,
};
use crate::{FontRhythm, Rhythm, RhythmBlockMetrics, RhythmLineMetrics};
const DEFAULT_RHYTHM_OVERLAY_RGBA: u32 = 0xff78783f;
fn assert_valid_font_request(font: &Font, font_size: Pixels, line_rhythms: u32) {
assert!(
font.weight.0.is_finite() && font.weight.0 > 0.0,
"font weight must be finite and greater than zero"
);
let font_size: f32 = font_size.into();
assert!(
font_size.is_finite() && font_size > 0.0,
"font_size must be finite and greater than zero"
);
assert!(line_rhythms > 0, "line_rhythms must be greater than zero");
}
fn ideographic_ink_ascent(bounds: Bounds<Pixels>) -> Option<f32> {
let bottom = f32::from(bounds.origin.y);
let top = f32::from(bounds.origin.y + bounds.size.height);
(bottom.is_finite() && bottom < 0.0 && top.is_finite() && top > 0.0).then_some(top)
}
fn select_icf_ascent<E>(
bounds: impl IntoIterator<Item = Result<Bounds<Pixels>, E>>,
) -> Result<f32, IcfMeasurementError> {
let mut saw_bounds = false;
let mut ascent: Option<f32> = None;
for bounds in bounds {
let Ok(bounds) = bounds else {
continue;
};
saw_bounds = true;
if let Some(candidate) = ideographic_ink_ascent(bounds) {
ascent = Some(ascent.map_or(candidate, |current| current.max(candidate)));
}
}
match ascent {
Some(ascent) => Ok(ascent),
None if saw_bounds => Err(IcfMeasurementError::NoUsableBounds),
None => Err(IcfMeasurementError::NoProbeBounds),
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RhythmGrid {
core: Rhythm,
}
impl RhythmGrid {
pub fn new(size: Pixels) -> Self {
Self {
core: Rhythm::new(size.into()),
}
}
#[inline]
pub fn size(&self) -> Pixels {
px(self.core.size())
}
#[inline]
pub const fn rhythm(&self) -> Rhythm {
self.core
}
#[inline]
pub fn spacing(&self, n: i32) -> Pixels {
px(self.rhythm().spacing(n))
}
#[inline]
pub fn height(&self, n: i32) -> Pixels {
self.spacing(n)
}
#[inline]
pub fn snap_up(&self, height: Pixels) -> Pixels {
px(self.rhythm().snap_up(height.into()))
}
#[inline]
pub fn snap_down(&self, height: Pixels) -> Pixels {
px(self.rhythm().snap_down(height.into()))
}
pub fn line_metrics(
&self,
ascent: Pixels,
descent: Pixels,
line_rhythms: u32,
) -> RhythmLineMetrics {
RhythmLineMetrics::new(ascent.into(), descent.into(), line_rhythms, self.rhythm())
}
pub fn line_metrics_at_least(
&self,
ascent: Pixels,
descent: Pixels,
line_rhythms: u32,
) -> RhythmLineMetrics {
RhythmLineMetrics::at_least(ascent.into(), descent.into(), line_rhythms, self.rhythm())
}
pub fn line_metrics_covering(&self, metrics: &[RhythmLineMetrics]) -> RhythmLineMetrics {
RhythmLineMetrics::covering(metrics, self.rhythm())
}
pub fn font(
&self,
text_system: &TextSystem,
font: Font,
font_size: Pixels,
line_rhythms: u32,
) -> RhythmFont {
RhythmFont::resolve(text_system, font, font_size, line_rhythms, *self)
}
pub fn overlay(&self, color: impl Into<Hsla>) -> RhythmOverlay {
rhythm_overlay(*self, color)
}
}
impl RhythmLineMetrics {
#[inline]
pub fn line_height_px(&self) -> Pixels {
px(self.line_height())
}
#[inline]
pub fn paint_origin_for_px(&self, target_baseline: Pixels) -> Pixels {
px(self.paint_origin_for(target_baseline.into()))
}
}
impl RhythmBlockMetrics {
#[inline]
pub fn first_baseline_px(&self) -> Pixels {
px(self.first_baseline())
}
#[inline]
pub fn baseline_at_row_px(&self, row: i64) -> Pixels {
px(self.baseline_at_row(row))
}
}
#[derive(Debug, Clone)]
pub struct RhythmFont {
font: Font,
font_id: Option<FontId>,
metrics: FontRhythm,
grid: RhythmGrid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IcfMeasurementError {
UnresolvedFont,
EmptyProbes,
NoProbeBounds,
NoUsableBounds,
}
impl std::fmt::Display for IcfMeasurementError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::UnresolvedFont => "the rhythm font has no TextSystem-resolved font identity",
Self::EmptyProbes => "no ICF probe glyphs were supplied",
Self::NoProbeBounds => "the text backend returned no bounds for any ICF probe",
Self::NoUsableBounds => {
"probe bounds were returned, but none described usable glyph ink"
}
})
}
}
impl std::error::Error for IcfMeasurementError {}
#[derive(Debug, Clone)]
pub struct RhythmIcfAnchor {
font: RhythmFont,
ascent: Pixels,
}
impl RhythmIcfAnchor {
#[inline]
pub const fn font(&self) -> &RhythmFont {
&self.font
}
#[inline]
pub fn span(&self, top: i32, bottom: i32) -> (Pixels, Pixels) {
let block = RhythmBlockMetrics::ink_anchored(
self.font.line_metrics(),
f32::from(self.ascent),
top,
bottom,
);
(px(block.opening()), px(block.closing()))
}
#[inline]
pub fn trim_top(&self) -> Pixels {
self.font.baseline_above() - self.ascent
}
}
impl RhythmFont {
pub fn resolve(
text_system: &TextSystem,
font: Font,
font_size: Pixels,
line_rhythms: u32,
grid: RhythmGrid,
) -> Self {
assert_valid_font_request(&font, font_size, line_rhythms);
let font_id = text_system.resolve_font(&font);
let metrics = FontRhythm::from_platform_metrics(
font_size.into(),
line_rhythms,
text_system.ascent(font_id, font_size).into(),
text_system.descent(font_id, font_size).into(),
text_system.cap_height(font_id, font_size).into(),
text_system.x_height(font_id, font_size).into(),
);
Self {
font,
font_id: Some(font_id),
metrics,
grid,
}
}
pub fn from_baseline_ratio(
font: Font,
font_size: Pixels,
line_rhythms: u32,
baseline_ratio: f32,
grid: RhythmGrid,
) -> Self {
assert_valid_font_request(&font, font_size, line_rhythms);
Self {
font,
font_id: None,
metrics: FontRhythm::from_baseline_ratio(
font_size.into(),
line_rhythms,
baseline_ratio,
),
grid,
}
}
pub fn measure_icf(
&self,
text_system: &TextSystem,
probes: &str,
) -> Result<RhythmIcfAnchor, IcfMeasurementError> {
let font_id = self.font_id.ok_or(IcfMeasurementError::UnresolvedFont)?;
if probes.is_empty() {
return Err(IcfMeasurementError::EmptyProbes);
}
let font_size = self.font_size();
let ascent = select_icf_ascent(
probes
.chars()
.map(|ch| text_system.typographic_bounds(font_id, font_size, ch)),
)?;
Ok(RhythmIcfAnchor {
font: self.clone(),
ascent: px(ascent),
})
}
pub fn font(&self) -> &Font {
&self.font
}
#[inline]
pub const fn resolved_font_id(&self) -> Option<FontId> {
self.font_id
}
pub fn spec(&self) -> Option<RhythmFontSpec> {
self.font_id?;
Some(RhythmFontSpec {
font: self.font.clone(),
font_size: self.font_size(),
line_rhythms: self.metrics.line_rhythms(),
grid_size: self.grid.size(),
})
}
pub fn line_metrics(&self) -> RhythmLineMetrics {
self.metrics.line_metrics(self.grid.rhythm())
}
#[inline]
pub const fn grid(&self) -> RhythmGrid {
self.grid
}
#[inline]
pub fn baseline_top(&self, n: i32) -> Pixels {
px(self.metrics.baseline_top(self.grid.rhythm(), n))
}
#[inline]
pub fn baseline_bottom(&self, n: i32) -> Pixels {
px(self.metrics.baseline_bottom(self.grid.rhythm(), n))
}
#[inline]
pub fn baseline_between(&self, below: &RhythmFont, n: i32) -> Pixels {
assert_eq!(
self.grid, below.grid,
"both fonts must be resolved against the same grid size"
);
px(self
.metrics
.baseline_between(self.grid.rhythm(), &below.metrics, n))
}
#[inline]
pub fn cap_top(&self, n: i32) -> Option<Pixels> {
self.metrics.cap_top(self.grid.rhythm(), n).map(px)
}
#[inline]
pub fn cap_bottom(&self, n: i32) -> Option<Pixels> {
self.metrics.cap_bottom(self.grid.rhythm(), n).map(px)
}
#[inline]
pub fn cap_span(&self, top: i32, bottom: i32) -> Option<(Pixels, Pixels)> {
Some((self.cap_top(top)?, self.cap_bottom(bottom)?))
}
pub fn drop_cap(&self, text_system: &TextSystem, font: Font, lines: u32) -> RhythmDropCap {
RhythmDropCap::resolve(text_system, font, self, lines)
}
#[inline]
pub const fn metrics(&self) -> &FontRhythm {
&self.metrics
}
#[inline]
pub fn font_size(&self) -> Pixels {
px(self.metrics.font_size())
}
#[inline]
pub fn line_height(&self) -> Pixels {
px(self.metrics.line_height(self.grid.rhythm()))
}
#[inline]
pub fn baseline_above(&self) -> Pixels {
px(self.metrics.baseline_above(self.grid.rhythm()))
}
#[inline]
pub fn baseline_below(&self) -> Pixels {
px(self.metrics.baseline_below(self.grid.rhythm()))
}
#[inline]
pub fn cap_trim_top(&self) -> Option<Pixels> {
self.metrics.cap_trim_top(self.grid.rhythm()).map(px)
}
#[inline]
pub fn x_trim_top(&self) -> Option<Pixels> {
self.metrics.x_trim_top(self.grid.rhythm()).map(px)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RhythmFontSpec {
font: Font,
font_size: Pixels,
line_rhythms: u32,
grid_size: Pixels,
}
impl RhythmFontSpec {
pub fn new(font: Font, font_size: Pixels, line_rhythms: u32, grid: RhythmGrid) -> Self {
assert_valid_font_request(&font, font_size, line_rhythms);
Self {
font,
font_size,
line_rhythms,
grid_size: grid.size(),
}
}
pub fn font(&self) -> &Font {
&self.font
}
#[inline]
pub const fn font_size(&self) -> Pixels {
self.font_size
}
#[inline]
pub const fn line_rhythms(&self) -> u32 {
self.line_rhythms
}
#[inline]
pub fn grid(&self) -> RhythmGrid {
RhythmGrid::new(self.grid_size)
}
pub fn resolve(&self, text_system: &TextSystem) -> RhythmFont {
RhythmFont::resolve(
text_system,
self.font.clone(),
self.font_size,
self.line_rhythms,
self.grid(),
)
}
pub fn resolve_covering(
&self,
text_system: &TextSystem,
others: &[RhythmFontSpec],
) -> RhythmFont {
for spec in others {
assert_eq!(
spec.grid_size, self.grid_size,
"every covering font spec must use the primary grid size"
);
assert_eq!(
spec.font_size, self.font_size,
"every covering font spec must use the primary font size"
);
}
let primary = self.resolve(text_system);
let mut covered = Vec::with_capacity(others.len() + 1);
covered.push(primary.line_metrics());
covered.extend(
others
.iter()
.map(|spec| spec.resolve(text_system).line_metrics()),
);
let line_rhythms =
RhythmLineMetrics::covering(&covered, self.grid().rhythm()).line_rhythms();
if line_rhythms == self.line_rhythms {
return primary;
}
Self {
line_rhythms,
..self.clone()
}
.resolve(text_system)
}
}
#[derive(Debug, Clone)]
pub struct RhythmDropCap {
font: RhythmFont,
top: Pixels,
}
impl RhythmDropCap {
pub fn resolve(text_system: &TextSystem, font: Font, body: &RhythmFont, lines: u32) -> Self {
let probe = RhythmFont::resolve(text_system, font.clone(), body.font_size(), 1, body.grid);
let solved = body
.metrics()
.drop_cap(body.grid.rhythm(), probe.metrics(), lines);
Self {
font: RhythmFont {
font,
font_id: probe.font_id,
metrics: *solved.metrics(),
grid: body.grid,
},
top: px(solved.top()),
}
}
pub const fn font(&self) -> &RhythmFont {
&self.font
}
#[inline]
pub const fn top(&self) -> Pixels {
self.top
}
}
pub trait RhythmStyled: Styled + Sized {
fn rhythm_font(self, font: &RhythmFont) -> Self {
self.font(font.font().clone())
.text_size(font.font_size())
.line_height(font.line_height())
}
fn rhythm_block(self, font: &RhythmFont, top: i32, bottom: i32) -> Self {
self.rhythm_font(font)
.pt(font.baseline_top(top))
.pb(font.baseline_bottom(bottom))
}
fn rhythm_drop_cap(self, cap: &RhythmDropCap) -> Self {
self.rhythm_font(cap.font()).relative().top(cap.top())
}
fn rhythm_debug_overlay(self, overlay: impl Into<RhythmOverlay>, show: bool) -> Self
where
Self: ParentElement,
{
if show {
self.child(overlay.into())
} else {
self
}
}
}
impl<T: Styled> RhythmStyled for T {}
pub fn rhythm_overlay(grid: RhythmGrid, color: impl Into<Hsla>) -> RhythmOverlay {
RhythmOverlay {
grid,
color: color.into(),
phase: px(0.),
}
}
#[derive(IntoElement)]
pub struct RhythmOverlay {
grid: RhythmGrid,
color: Hsla,
phase: Pixels,
}
impl RhythmOverlay {
#[must_use]
pub fn phase(mut self, offset: Pixels) -> Self {
assert!(
f32::from(offset).is_finite(),
"overlay phase must be finite"
);
self.phase = offset;
self
}
pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
let visible = bounds.intersect(&window.content_mask().bounds);
if visible.size.height <= px(0.) || visible.size.width <= px(0.) {
return;
}
let origin_y = overlay_origin_y(bounds.origin.y, self.phase);
visible_stripes(
origin_y,
f32::from(self.grid.size()),
f32::from(visible.origin.y),
f32::from(visible.bottom()),
|from, to| {
window.paint_quad(fill(
Bounds::new(
point(bounds.origin.x, px(from)),
size(bounds.size.width, px(to - from)),
),
self.color,
));
},
);
}
}
#[inline]
fn overlay_origin_y(element_top: Pixels, phase: Pixels) -> f32 {
f32::from(element_top) + f32::from(phase)
}
impl From<RhythmGrid> for RhythmOverlay {
fn from(grid: RhythmGrid) -> Self {
rhythm_overlay(grid, rgba(DEFAULT_RHYTHM_OVERLAY_RGBA))
}
}
impl RenderOnce for RhythmOverlay {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
canvas(
|_, _, _| (),
move |bounds, _, window, _| self.paint(bounds, window),
)
.absolute()
.inset_0()
}
}
fn visible_stripes(
origin_y: f32,
stripe_height: f32,
top: f32,
bottom: f32,
mut paint: impl FnMut(f32, f32),
) {
let step = stripe_height * 2.0;
let skipped = (((top - origin_y - stripe_height) / step).ceil()).max(0.0);
let mut y = origin_y + step * skipped;
while y < bottom {
let from = y.max(top);
let to = (y + stripe_height).min(bottom);
if to > from {
paint(from, to);
}
let next = y + step;
if !next.is_finite() || next <= y {
break;
}
y = next;
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{
font, AnyElement, FontFallbacks, FontFeatures, FontId, FontStyle, FontWeight, Position,
StyleRefinement,
};
#[derive(Default)]
struct CapturedStyle {
style: StyleRefinement,
}
impl Styled for CapturedStyle {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
#[test]
fn pixels_mirrors_agree_with_the_math_layer() {
let grid = RhythmGrid::new(px(8.0));
assert_eq!(grid.rhythm(), Rhythm::new(8.0));
assert_eq!(grid.spacing(5), px(40.0));
assert_eq!(grid.height(5), grid.spacing(5));
let line = grid.line_metrics(px(14.67), px(3.51), 3);
assert_eq!(line.line_height_px(), px(line.line_height()));
let target = grid.height(5);
assert_eq!(
line.paint_origin_for_px(target),
px(line.paint_origin_for(target.into()))
);
let block = RhythmBlockMetrics::new(line, 3, 1);
assert_eq!(block.first_baseline_px(), px(block.first_baseline()));
let wide_row = i64::from(i32::MAX) * 4;
assert_eq!(
block.baseline_at_row_px(wide_row),
px(block.baseline_at_row(wide_row))
);
}
#[test]
fn line_metrics_grid_helpers_match_the_math_layer() {
let grid = RhythmGrid::new(px(8.0));
let grown = grid.line_metrics_at_least(px(20.0), px(6.0), 3);
assert_eq!(grown.line_rhythms(), 4);
assert!(!grown.overflows_line_box());
let body = grid.line_metrics(px(14.67), px(3.51), 3);
let display = grid.line_metrics(px(28.8), px(6.4), 3);
assert_eq!(
grid.line_metrics_covering(&[body, display]),
RhythmLineMetrics::covering(&[body, display], body.grid())
);
}
#[test]
fn spacing_methods_agree_with_the_math_layer() {
let grid = RhythmGrid::new(px(8.0));
let rhythm = grid.rhythm();
let metrics = FontRhythm::from_platform_metrics(16.0, 3, 14.67, -3.51, 11.09, 7.70);
let body = RhythmFont {
font: font("Example Serif"),
font_id: None,
metrics,
grid,
};
let below = RhythmFont::from_baseline_ratio(font("Example Serif"), px(24.0), 4, 0.2, grid);
assert_eq!(body.baseline_top(3), px(metrics.baseline_top(rhythm, 3)));
assert_eq!(
body.baseline_bottom(1),
px(metrics.baseline_bottom(rhythm, 1))
);
assert_eq!(
body.baseline_between(&below, 6),
px(metrics.baseline_between(rhythm, below.metrics(), 6))
);
assert_eq!(body.cap_top(3), metrics.cap_top(rhythm, 3).map(px));
assert_eq!(body.cap_bottom(0), metrics.cap_bottom(rhythm, 0).map(px));
}
#[test]
fn rhythm_font_applies_the_resolved_font_contract() {
let mut resolved_font = font("Example Serif");
resolved_font.features = FontFeatures::disable_ligatures();
resolved_font.fallbacks = Some(FontFallbacks::from_fonts(vec!["Fallback Serif".into()]));
resolved_font.style = FontStyle::Oblique;
let expected = resolved_font.clone();
let rhythm_font = RhythmFont::from_baseline_ratio(
resolved_font,
px(16.0),
3,
0.2,
RhythmGrid::new(px(8.0)),
);
let captured = CapturedStyle::default().rhythm_font(&rhythm_font);
let text = captured
.style
.text
.expect("rhythm font should set text style");
assert_eq!(text.font_family, Some(expected.family));
assert_eq!(text.font_features, Some(expected.features));
assert_eq!(text.font_fallbacks, expected.fallbacks);
assert_eq!(text.font_weight, Some(expected.weight));
assert_eq!(text.font_style, Some(expected.style));
}
#[derive(Default)]
struct CapturedChildren {
style: StyleRefinement,
children: Vec<AnyElement>,
}
impl Styled for CapturedChildren {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for CapturedChildren {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
#[test]
fn rhythm_debug_overlay_appends_only_when_shown() {
let grid = RhythmGrid::new(px(8.0));
let hidden = CapturedChildren::default().rhythm_debug_overlay(grid, false);
assert!(hidden.children.is_empty());
let shown = CapturedChildren::default().rhythm_debug_overlay(grid, true);
assert_eq!(shown.children.len(), 1);
let color = rgba(0x0969da33);
let hidden = CapturedChildren::default().rhythm_debug_overlay(grid.overlay(color), false);
assert!(hidden.children.is_empty());
let shown = CapturedChildren::default().rhythm_debug_overlay(grid.overlay(color), true);
assert_eq!(shown.children.len(), 1);
}
#[test]
fn rhythm_overlay_keeps_the_requested_grid_and_color() {
let grid = RhythmGrid::new(px(10.0));
let color: Hsla = rgba(0x0969da33).into();
let overlay = rhythm_overlay(grid, color);
assert_eq!(overlay.grid, grid);
assert_eq!(overlay.color, color);
let mut spans = Vec::new();
visible_stripes(
0.0,
f32::from(overlay.grid.size()),
0.0,
45.0,
|from, to| spans.push((from, to)),
);
assert_eq!(spans, [(0.0, 10.0), (20.0, 30.0), (40.0, 45.0)]);
let factory = grid.overlay(color);
assert_eq!(factory.grid, grid);
assert_eq!(factory.color, color);
assert_eq!(factory.phase, px(0.));
}
#[test]
fn a_bare_grid_converts_to_the_default_red_overlay() {
let overlay = RhythmOverlay::from(RhythmGrid::new(px(8.0)));
assert_eq!(overlay.color, rgba(DEFAULT_RHYTHM_OVERLAY_RGBA).into());
assert_eq!(overlay.phase, px(0.));
}
#[test]
fn rhythm_drop_cap_anchors_with_a_relative_inset_not_a_margin() {
let grid = RhythmGrid::new(px(8.0));
let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
let solved = body.metrics().drop_cap(Rhythm::new(8.0), body.metrics(), 3);
let cap = RhythmDropCap {
font: RhythmFont {
font: font("Example Serif"),
font_id: None,
metrics: *solved.metrics(),
grid,
},
top: px(solved.top()),
};
let captured = CapturedStyle::default().rhythm_drop_cap(&cap);
assert_eq!(captured.style.position, Some(Position::Relative));
assert_eq!(captured.style.inset.top, Some(cap.top().into()));
assert_eq!(captured.style.margin.top, None);
let text = captured.style.text.expect("drop cap should set text style");
assert_eq!(text.font_family, Some("Example Serif".into()));
}
#[test]
#[should_panic(expected = "same grid size")]
fn baseline_between_rejects_fonts_on_different_grids() {
let above = RhythmFont::from_baseline_ratio(
font("Example Serif"),
px(16.0),
3,
0.2,
RhythmGrid::new(px(8.0)),
);
let below = RhythmFont::from_baseline_ratio(
font("Example Serif"),
px(16.0),
3,
0.2,
RhythmGrid::new(px(10.0)),
);
above.baseline_between(&below, 3);
}
#[test]
fn rhythm_block_applies_the_font_and_the_paired_paddings() {
let grid = RhythmGrid::new(px(8.0));
let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
let captured = CapturedStyle::default().rhythm_block(&body, 3, 1);
assert_eq!(
captured.style.padding.top,
Some(body.baseline_top(3).into())
);
assert_eq!(
captured.style.padding.bottom,
Some(body.baseline_bottom(1).into())
);
let text = captured
.style
.text
.expect("rhythm block should set the text style");
assert_eq!(text.font_family, Some("Example Serif".into()));
}
#[test]
fn cap_span_pairs_the_anchors_and_spans_whole_rows() {
let grid = RhythmGrid::new(px(8.0));
let metrics = FontRhythm::from_platform_metrics(16.0, 3, 14.67, -3.51, 11.09, 7.70);
let heading = RhythmFont {
font: font("Example Serif"),
font_id: None,
metrics,
grid,
};
let (pt, pb) = heading.cap_span(3, 0).expect("cap metrics are present");
assert_eq!(Some(pt), heading.cap_top(3));
assert_eq!(Some(pb), heading.cap_bottom(0));
let rows = f32::from(pt + heading.line_height() + pb) / 8.0;
assert!((rows - rows.round()).abs() < 1e-3);
let line = metrics.line_metrics(grid.rhythm());
let block =
RhythmBlockMetrics::ink_anchored(line, metrics.cap_height().expect("cap height"), 3, 0);
assert!((f32::from(pt) - block.opening()).abs() < 1e-4);
assert!((f32::from(pb) - block.closing()).abs() < 1e-4);
}
#[test]
fn cap_spacing_returns_none_without_cap_height() {
let grid = RhythmGrid::new(px(8.0));
let font = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
assert_eq!(font.cap_top(3), None);
assert_eq!(font.cap_bottom(1), None);
assert_eq!(font.cap_span(3, 1), None);
}
const PINGFANG_ICF_16: f32 = 0.822 * 16.0;
fn pingfang_icf_16(grid: RhythmGrid) -> RhythmIcfAnchor {
RhythmIcfAnchor {
font: RhythmFont {
font: font("Example CJK"),
font_id: None,
metrics: FontRhythm::from_platform_metrics(16.0, 3, 16.96, -5.44, 13.76, 9.6),
grid,
},
ascent: px(PINGFANG_ICF_16),
}
}
#[test]
fn icf_anchor_pairs_the_anchors_and_spans_whole_rows() {
let grid = RhythmGrid::new(px(8.0));
let anchor = pingfang_icf_16(grid);
let body = anchor.font();
let (pt, pb) = anchor.span(3, 0);
let trim = anchor.trim_top();
assert_eq!(pt, grid.height(3) - trim);
assert_eq!(pb, trim);
assert_eq!(pt + trim, grid.height(3));
let rows = f32::from(pt + body.line_height() + pb) / 8.0;
assert!((rows - rows.round()).abs() < 1e-3);
assert_ne!(body.cap_top(3), Some(pt));
}
#[test]
fn icf_anchor_agrees_with_the_generic_block_metrics() {
let grid = RhythmGrid::new(px(8.0));
let anchor = pingfang_icf_16(grid);
let body = anchor.font();
let (pt, pb) = anchor.span(3, 0);
let line = body.metrics().line_metrics(grid.rhythm());
let block = RhythmBlockMetrics::ink_anchored(line, PINGFANG_ICF_16, 3, 0);
assert!((f32::from(pt) - block.opening()).abs() < 1e-4);
assert!((f32::from(pb) - block.closing()).abs() < 1e-4);
}
#[test]
fn icf_measurement_rejects_advance_only_placeholder_bounds() {
let real_ink = Bounds {
origin: point(px(0.0), px(-1.6)),
size: size(px(16.0), px(14.8)),
};
assert!((ideographic_ink_ascent(real_ink).unwrap() - 13.2).abs() < 1e-4);
let advance_only = Bounds {
origin: point(px(0.0), px(0.0)),
size: size(px(16.0), px(16.0)),
};
assert_eq!(ideographic_ink_ascent(advance_only), None);
assert_eq!(
select_icf_ascent([Err::<Bounds<Pixels>, ()>(())]),
Err(IcfMeasurementError::NoProbeBounds)
);
assert_eq!(
select_icf_ascent([Ok::<Bounds<Pixels>, ()>(advance_only)]),
Err(IcfMeasurementError::NoUsableBounds)
);
assert_eq!(
select_icf_ascent([Err(()), Ok(advance_only)]),
Err(IcfMeasurementError::NoUsableBounds),
"a returned-but-rejected bound wins over failed probe queries"
);
assert!(
(select_icf_ascent([Err(()), Ok(real_ink)]).unwrap() - 13.2).abs() < 1e-4,
"one usable probe makes the aggregate measurement succeed"
);
}
#[test]
#[should_panic(expected = "rhythm unit size must be finite and greater than zero")]
fn grid_rejects_a_non_positive_size() {
let _ = RhythmGrid::new(px(0.0));
}
fn stripe_spans(origin_y: f32, top: f32, bottom: f32) -> Vec<(f32, f32)> {
let mut spans = Vec::new();
visible_stripes(origin_y, 8.0, top, bottom, |from, to| {
spans.push((from, to))
});
spans
}
#[test]
fn overlay_stripes_every_other_row_from_the_container_top() {
assert_eq!(
stripe_spans(0.0, 0.0, 40.0),
[(0.0, 8.0), (16.0, 24.0), (32.0, 40.0)]
);
assert_eq!(
stripe_spans(0.0, 0.0, 36.0),
[(0.0, 8.0), (16.0, 24.0), (32.0, 36.0)]
);
}
#[test]
fn overlay_walks_only_the_visible_region_and_keeps_the_phase() {
assert_eq!(
stripe_spans(0.0, 1607.0, 1630.0),
[(1607.0, 1608.0), (1616.0, 1624.0)]
);
assert_eq!(stripe_spans(0.0, 1609.0, 1630.0), [(1616.0, 1624.0)]);
}
#[test]
fn overlay_phase_uses_the_contents_signed_translation() {
let grid = RhythmGrid::new(px(8.0));
let one_row = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-8.0));
let one_row_origin = overlay_origin_y(px(0.0), one_row.phase);
assert_eq!(one_row_origin, -8.0);
assert_eq!(
stripe_spans(one_row_origin, 0.0, 40.0),
[(8.0, 16.0), (24.0, 32.0)]
);
let half_row = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-4.0));
let half_row_origin = overlay_origin_y(px(0.0), half_row.phase);
assert_eq!(
stripe_spans(half_row_origin, 0.0, 40.0),
[(0.0, 4.0), (12.0, 20.0), (28.0, 36.0)]
);
let down = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(4.0));
let down_origin = overlay_origin_y(px(0.0), down.phase);
assert_eq!(down_origin, 4.0);
assert_eq!(
stripe_spans(down_origin, 0.0, 24.0),
[(4.0, 12.0), (20.0, 24.0)]
);
let deep = rhythm_overlay(grid, rgba(0xff78783f)).phase(px(-1600.0));
let deep_origin = overlay_origin_y(px(0.0), deep.phase);
assert_eq!(
stripe_spans(deep_origin, 0.0, 24.0),
[(0.0, 8.0), (16.0, 24.0)]
);
}
#[test]
fn overlay_stops_when_a_stride_is_below_coordinate_precision() {
assert_eq!(100.0f32 + 2.0e-7, 100.0);
let mut spans = Vec::new();
visible_stripes(0.0, 1.0e-7, 100.0, 101.0, |from, to| spans.push((from, to)));
assert!(spans.is_empty());
}
#[test]
#[should_panic(expected = "overlay phase must be finite")]
fn overlay_rejects_a_non_finite_phase() {
let _ = rhythm_overlay(RhythmGrid::new(px(8.0)), rgba(0xff78783f)).phase(px(f32::NAN));
}
#[test]
fn covering_reaches_the_math_layer_from_grid_built_lines() {
let grid = RhythmGrid::new(px(8.0));
let body = grid.line_metrics(px(14.67), px(3.51), 3);
let display = grid.line_metrics(px(28.8), px(6.4), 3);
let covering = grid.line_metrics_covering(&[body, display]);
assert_eq!(covering.line_rhythms(), 5);
assert!(!grid
.line_metrics(px(28.8), px(6.4), covering.line_rhythms())
.overflows_line_box());
}
#[test]
fn spec_keys_a_map_for_text_system_resolved_fonts() {
use std::collections::HashMap;
let grid = RhythmGrid::new(px(8.0));
let metrics = FontRhythm::from_baseline_ratio(16.0, 3, 0.2);
let body = RhythmFont {
font: font("Example Serif"),
font_id: Some(FontId(7)),
metrics,
grid,
};
let spec = body.spec().expect("font came from a text system");
assert_eq!(
spec,
RhythmFontSpec::new(font("Example Serif"), px(16.0), 3, grid)
);
assert_eq!(spec.font(), body.font());
assert_eq!(spec.font_size(), body.font_size());
assert_eq!(spec.line_rhythms(), 3);
assert_eq!(spec.grid(), grid);
let mut cache = HashMap::new();
cache.insert(spec.clone(), body);
assert!(cache.contains_key(&spec));
let other = RhythmFontSpec::new(font("Example Serif"), px(17.0), 3, grid);
assert!(!cache.contains_key(&other));
}
#[test]
fn spec_rejects_invalid_request_values_before_it_can_be_a_cache_key() {
let grid = RhythmGrid::new(px(8.0));
for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
let result = std::panic::catch_unwind(|| {
RhythmFontSpec::new(font("Example Serif"), px(invalid), 3, grid)
});
assert!(result.is_err(), "accepted invalid font size {invalid:?}");
}
let zero_lines = std::panic::catch_unwind(|| {
RhythmFontSpec::new(font("Example Serif"), px(16.0), 0, grid)
});
assert!(zero_lines.is_err(), "accepted a zero line-rhythm count");
for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
let mut invalid_font = font("Example Serif");
invalid_font.weight = FontWeight(invalid);
let result =
std::panic::catch_unwind(|| RhythmFontSpec::new(invalid_font, px(16.0), 3, grid));
assert!(result.is_err(), "accepted invalid font weight {invalid:?}");
}
}
#[test]
fn baseline_ratio_font_rejects_an_invalid_font_weight() {
let grid = RhythmGrid::new(px(8.0));
for invalid in [0.0, -0.0, -1.0, f32::NAN, f32::INFINITY] {
let mut invalid_font = font("Example Serif");
invalid_font.weight = FontWeight(invalid);
let result = std::panic::catch_unwind(|| {
RhythmFont::from_baseline_ratio(invalid_font, px(16.0), 3, 0.2, grid)
});
assert!(result.is_err(), "accepted invalid font weight {invalid:?}");
}
}
#[test]
fn baseline_ratio_fonts_carry_no_resolved_identity() {
let grid = RhythmGrid::new(px(8.0));
let first = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
let second = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.3, grid);
assert_eq!(first.resolved_font_id(), None);
assert_eq!(first.spec(), None);
assert_eq!(second.spec(), None);
assert_ne!(first.metrics(), second.metrics());
}
#[test]
fn line_metric_factories_agree_with_the_math_layer() {
let grid = RhythmGrid::new(px(8.0));
let body = RhythmFont::from_baseline_ratio(font("Example Serif"), px(16.0), 3, 0.2, grid);
let from_font = body.line_metrics();
let from_grid =
grid.line_metrics(px(body.metrics().ascent()), px(body.metrics().descent()), 3);
assert_eq!(from_font, from_grid);
assert_eq!(f32::from(body.baseline_above()), from_font.baseline_above());
assert_eq!(f32::from(body.baseline_below()), from_font.baseline_below());
assert_eq!(f32::from(body.line_height()), from_font.line_height());
}
}