//! The terminal cell, measured off the face rather than guessed. //! //! The cell comes from the face. The face is the only thing that knows how wide //! its own glyphs are, so a cell derived from it cannot drift when the font is //! replaced, and a hardcoded one always does. //! //! Cell furniture spans the full advance: `─` and `█` both cover the whole cell //! width, because that is what they are for. A cell one pixel wider than the //! advance therefore puts a hole every column in a run of `───` and draws `███` //! as vertical stripes. Box drawing that does not tile is the single most //! visible thing a terminal can get wrong. use swash::FontRef; /// A terminal cell, in logical pixels at one font size. #[derive(Debug, Clone, Copy, PartialEq)] pub struct CellMetrics { /// Cell width: the face's own advance, rounded to a whole pixel. pub advance: f32, /// Cell height: the face's line height, rounded up to a whole pixel. pub height: f32, /// The face's own ascent at this size, unrounded. pub ascent: f32, /// Where the baseline sits, measured down from the top of the cell. /// /// Not the same as [`ascent`](Self::ascent). The cell is the line height /// rounded up, so it has a little more room than the face asked for, and /// that slack is shared between the two sides in the proportion the face /// itself uses rather than all being dropped below the descender. It is /// also what makes the cell-furniture snap exact: a glyph scaled about the /// baseline by `height / exact_height` reaches the top and the bottom of /// the cell precisely when the baseline sits here. pub baseline: f32, /// The advance before rounding, kept so the residual is measurable rather /// than folklore. See [`rounding_error`](Self::rounding_error). pub exact_advance: f32, /// The line height before rounding. pub exact_height: f32, } impl CellMetrics { /// Measure a face at a size. /// /// Takes bytes rather than a live renderer on purpose: `shop` needs the /// cell before it has a wgpu device, because the PTY is sized in cells and /// is spawned first. pub fn measure(font_data: &[u8], px: f32) -> anyhow::Result { let font = FontRef::from_index(font_data, 0) .ok_or_else(|| anyhow::anyhow!("swash: not a font"))?; let metrics = font.metrics(&[]).scale(px); // One representative glyph rather than `average_width` or `max_width`. // The grid has no concept of a per-glyph width anywhere outside a // cluster, so what is wanted is the advance every cell will actually // use, and in a monospace face `M` carries it. `average_width` is a // weighted figure that means nothing here, and `max_width` is whatever // the widest glyph in the file happens to be — in a Nerd Font, a // double-width icon. let exact_advance = font .glyph_metrics(&[]) .scale(px) .advance_width(font.charmap().map('M')); // Ascent, descent and leading, which is the line box the face asks for. // `descent` is positive in swash, so this is a sum. let exact_height = metrics.ascent + metrics.descent + metrics.leading; Ok(Self { // Rounded, because a cell has to be a whole number of pixels: a // fractional advance puts every column at a fractional x, and the // glyph is resampled rather than drawn. // // **Up, not to nearest**, for the same reason the height is: a cell // narrower than the advance makes every glyph overlap its right-hand // neighbour, which corrupts the per-row render cache, where a cell // wider than the advance only leaves a hairline of background that // the furniture snap closes anyway. // // This stopped being hypothetical when shop's bundled face changed. // IosevkaTerm advanced 1/2 em and landed whole at every even size, // so `round` never rounded down in practice. Quasi Mono advances // 79/125 em and 79 is prime, so it is whole at 125px and nowhere // usable — at 15px it measures 9.48 and `round` would have taken // half a pixel out of every column. advance: exact_advance.ceil().max(1.0), // Rounded *up*, not to nearest. A cell shorter than the line clips // the descenders of every row, and a glyph bleeding into the row // below breaks the per-row render cache; the cost of rounding up is // a sub-pixel of background between rows, which is the cheaper of // the two. height: exact_height.ceil().max(1.0), ascent: metrics.ascent, baseline: if exact_height > 0.0 { metrics.ascent * exact_height.ceil().max(1.0) / exact_height } else { metrics.ascent }, exact_advance, exact_height, }) } /// The same cell in physical pixels at an integer output scale. /// /// Every field scales, so the snap factors derived from it stay the ones /// the grid is actually stepping by. That matters more than it looks: the /// renderer is rebuilt at `font_px * scale`, and a face whose advance is /// fractional does not measure there to exactly `advance * scale`. Snapping /// to this rather than to the renderer's own measurement is what makes the /// two agree instead of drifting by a fraction of a pixel. #[must_use] pub fn scaled(self, scale: u32) -> Self { let s = scale.max(1) as f32; Self { advance: self.advance * s, height: self.height * s, ascent: self.ascent * s, baseline: self.baseline * s, exact_advance: self.exact_advance * s, exact_height: self.exact_height * s, } } /// How far the whole-pixel cell sits from the face's own metrics. /// /// Zero is not achievable in general and is not aimed for. An advance is /// a fraction of an em, so it lands on a whole pixel only at sizes that /// clear its denominator: IosevkaTerm is 1/2 em and does it at every even /// size, IBM Plex Mono is 3/5 and needs a multiple of 5, and Atkinson /// Hyperlegible Mono is 79/125 and does it at 125px and nowhere usable. /// Chasing it would mean choosing the font size to suit the face. /// /// So the renderer snaps cell furniture to the cell instead, and this /// number is what the snap absorbs. Kept because it says how hard the snap /// is working, which is worth a glance when a face is swapped. pub fn rounding_error(&self) -> (f32, f32) { ( self.advance - self.exact_advance, self.height - self.exact_height, ) } } #[cfg(test)] mod tests { use super::*; const FONT: &[u8] = shop_font::FACE; // The measurement the hardcoded constants got wrong, pinned so a font swap // cannot quietly reintroduce it. // // Stated as the face's own tables rather than as two numbers, because the // numbers moved when the bundled face did: IosevkaTerm is 1/2 em and 14px // gave exactly 7.0, Quasi Mono is 79/125 and gives 8.848. A test that // asserted 7.0 was asserting which font shop bundles, which is what the // rest of this file is for. #[test] fn the_bundled_face_measures_what_its_tables_say() { let font = FontRef::from_index(FONT, 0).unwrap(); let m = font.metrics(&[]).scale(14.0); let cell = CellMetrics::measure(FONT, 14.0).unwrap(); assert!( (cell.exact_advance - font .glyph_metrics(&[]) .scale(14.0) .advance_width(font.charmap().map('M'))) .abs() < 0.01, "advance {} is not the face's own", cell.exact_advance ); assert!( (cell.exact_height - (m.ascent + m.descent + m.leading)).abs() < 0.01, "height {} is not ascent + descent + leading", cell.exact_height ); assert!((cell.advance - cell.exact_advance.ceil()).abs() < f32::EPSILON); assert!((cell.height - cell.exact_height.ceil()).abs() < f32::EPSILON); } // The old constants, as a record of what was wrong rather than as a target. // // They were 8.0 x 17.0 and derived from no face at all, which is the defect: // against IosevkaTerm at 14px (7.0 x 17.5) every column carried a pixel of // dead space and every row lost half of one. That they happen to sit closer // to the face bundled today is luck and changes nothing — a number nothing // measures is wrong the moment the font moves, which it has since. #[test] fn the_old_constants_are_not_this_faces_cell_either() { let cell = CellMetrics::measure(FONT, 14.0).unwrap(); assert!( (8.0 - cell.exact_advance).abs() > 0.5, "the hardcoded 8.0 is {} off this face's advance", 8.0 - cell.exact_advance ); assert!( cell.exact_height > 17.0, "the hardcoded 17.0 clipped {} of every row", cell.exact_height - 17.0 ); } // The property the cell exists to have. Cell furniture is drawn to fill // its cell exactly, so a cell wider than the advance shows background // between every column: `───` gets a hole every cell and `███` draws as // stripes. Measured against the face's own box-drawing glyphs rather than // argued, because this is the failure the hardcoded 8.0 actually produced. #[test] fn box_drawing_fills_the_cell_it_is_given() { let font = FontRef::from_index(FONT, 0).unwrap(); let cell = CellMetrics::measure(FONT, 14.0).unwrap(); let glyphs = font.glyph_metrics(&[]).scale(14.0); let charmap = font.charmap(); for c in ['─', '│', '█', '┼', '╬'] { let advance = glyphs.advance_width(charmap.map(c)); assert!( (advance - cell.exact_advance).abs() < 0.01, "`{c}` advances {advance} in a {} cell", cell.exact_advance ); } } #[test] fn the_cell_scales_with_the_size_it_is_measured_at() { let one = CellMetrics::measure(FONT, 14.0).unwrap(); let two = CellMetrics::measure(FONT, 28.0).unwrap(); assert!((two.exact_advance - one.exact_advance * 2.0).abs() < 0.01); assert!((two.exact_height - one.exact_height * 2.0).abs() < 0.01); } #[test] fn a_cell_is_never_zero_even_at_an_absurd_size() { let cell = CellMetrics::measure(FONT, 0.1).unwrap(); assert!(cell.advance >= 1.0 && cell.height >= 1.0); } #[test] fn bytes_that_are_not_a_font_are_refused() { assert!(CellMetrics::measure(b"not a font", 14.0).is_err()); } }