use gpui::{
canvas, fill, point, px, rgba, size, Bounds, Font, Hsla, IntoElement, ParentElement, Pixels,
Styled, TextSystem,
};
use crate::{FontRhythm, Rhythm};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RhythmGrid {
core: Rhythm,
}
impl RhythmGrid {
pub fn new(size: Pixels) -> Self {
Self {
core: Rhythm::new(size.into()),
}
}
pub fn size(&self) -> Pixels {
px(self.core.size())
}
fn core(&self) -> Rhythm {
self.core
}
fn assert_font(&self, font: &RhythmFont) {
assert_eq!(
*self, font.grid,
"RhythmFont must use the same grid size as the spacing calculation"
);
}
pub fn height(&self, n: i32) -> Pixels {
px(self.core().height(n))
}
pub fn baseline_top(&self, font: &RhythmFont, n: i32) -> Pixels {
self.assert_font(font);
px(self.core().baseline_top(&font.metrics, n))
}
pub fn baseline_bottom(&self, font: &RhythmFont, n: i32) -> Pixels {
self.assert_font(font);
px(self.core().baseline_bottom(&font.metrics, n))
}
pub fn baseline_between(&self, above: &RhythmFont, below: &RhythmFont, n: i32) -> Pixels {
self.assert_font(above);
self.assert_font(below);
px(self
.core()
.baseline_between(&above.metrics, &below.metrics, n))
}
pub fn cap_top(&self, font: &RhythmFont, n: i32) -> Option<Pixels> {
self.assert_font(font);
self.core().cap_top(&font.metrics, n).map(px)
}
pub fn cap_bottom(&self, font: &RhythmFont, m: i32) -> Option<Pixels> {
self.assert_font(font);
self.core().cap_bottom(&font.metrics, m).map(px)
}
}
#[derive(Debug, Clone)]
pub struct RhythmFont {
font: Font,
metrics: FontRhythm,
grid: RhythmGrid,
}
impl RhythmFont {
pub fn resolve(
text_system: &TextSystem,
font: Font,
font_size: Pixels,
line_rhythms: u32,
grid: RhythmGrid,
) -> Self {
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,
metrics,
grid,
}
}
pub fn from_baseline_ratio(
font: Font,
font_size: Pixels,
line_rhythms: u32,
baseline_ratio: f32,
grid: RhythmGrid,
) -> Self {
Self {
font,
metrics: FontRhythm::from_baseline_ratio(
font_size.into(),
line_rhythms,
baseline_ratio,
),
grid,
}
}
pub fn font(&self) -> &Font {
&self.font
}
pub const fn metrics(&self) -> &FontRhythm {
&self.metrics
}
pub fn font_size(&self) -> Pixels {
px(self.metrics.font_size())
}
pub fn line_height(&self) -> Pixels {
px(self.metrics.line_height(self.grid.core()))
}
pub fn baseline_above(&self) -> Pixels {
px(self.metrics.baseline_above(self.grid.core()))
}
pub fn cap_trim_top(&self) -> Option<Pixels> {
self.metrics.cap_trim_top(self.grid.core()).map(px)
}
pub fn x_trim_top(&self) -> Option<Pixels> {
self.metrics.x_trim_top(self.grid.core()).map(px)
}
}
#[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(probe.metrics(), lines, body.grid.core());
Self {
font: RhythmFont {
font,
metrics: *solved.metrics(),
grid: body.grid,
},
top: px(solved.top()),
}
}
pub const fn font(&self) -> &RhythmFont {
&self.font
}
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_drop_cap(self, cap: &RhythmDropCap) -> Self {
self.rhythm_font(cap.font()).relative().top(cap.top())
}
fn rhythm_debug_overlay(self, grid: RhythmGrid, show: bool) -> Self
where
Self: ParentElement,
{
if show {
self.child(rhythm_overlay(grid, rgba(0xff78783f)))
} else {
self
}
}
}
impl<T: Styled> RhythmStyled for T {}
pub fn rhythm_overlay(grid: RhythmGrid, color: impl Into<Hsla>) -> impl IntoElement {
let color = color.into();
canvas(
|_, _, _| (),
move |bounds, _, window, _| {
let mut y = bounds.origin.y;
while y < bounds.bottom() {
window.paint_quad(fill(
Bounds::new(
point(bounds.origin.x, y),
size(bounds.size.width, grid.size()),
),
color,
));
y += grid.size() * 2.;
}
},
)
.absolute()
.inset_0()
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{
font, AnyElement, FontFallbacks, FontFeatures, FontStyle, Position, StyleRefinement,
};
#[derive(Default)]
struct CapturedStyle {
style: StyleRefinement,
}
impl Styled for CapturedStyle {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
#[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);
}
#[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(body.metrics(), 3, Rhythm::new(8.0));
let cap = RhythmDropCap {
font: RhythmFont {
font: font("Example Serif"),
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 = "RhythmFont must use the same grid size")]
fn spacing_rejects_a_font_bound_to_another_grid() {
let font = RhythmFont::from_baseline_ratio(
font("Example Serif"),
px(16.0),
3,
0.2,
RhythmGrid::new(px(8.0)),
);
RhythmGrid::new(px(10.0)).baseline_top(&font, 3);
}
#[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!(grid.cap_top(&font, 3), None);
assert_eq!(grid.cap_bottom(&font, 1), None);
}
#[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));
}
}