max / shop
| 1 | //! The terminal cell, measured off the face rather than guessed. |
| 2 | //! |
| 3 | //! The cell comes from the face. The face is the only thing that knows how wide |
| 4 | //! its own glyphs are, so a cell derived from it cannot drift when the font is |
| 5 | //! replaced, and a hardcoded one always does. |
| 6 | //! |
| 7 | //! Cell furniture spans the full advance: `─` and `█` both cover the whole cell |
| 8 | //! width, because that is what they are for. A cell one pixel wider than the |
| 9 | //! advance therefore puts a hole every column in a run of `───` and draws `███` |
| 10 | //! as vertical stripes. Box drawing that does not tile is the single most |
| 11 | //! visible thing a terminal can get wrong. |
| 12 | |
| 13 | use FontRef; |
| 14 | |
| 15 | /// A terminal cell, in logical pixels at one font size. |
| 16 | |
| 17 | |
| 18 | /// Cell width: the face's own advance, rounded to a whole pixel. |
| 19 | pub advance: f32, |
| 20 | /// Cell height: the face's line height, rounded up to a whole pixel. |
| 21 | pub height: f32, |
| 22 | /// The face's own ascent at this size, unrounded. |
| 23 | pub ascent: f32, |
| 24 | /// Where the baseline sits, measured down from the top of the cell. |
| 25 | /// |
| 26 | /// Not the same as [`ascent`](Self::ascent). The cell is the line height |
| 27 | /// rounded up, so it has a little more room than the face asked for, and |
| 28 | /// that slack is shared between the two sides in the proportion the face |
| 29 | /// itself uses rather than all being dropped below the descender. It is |
| 30 | /// also what makes the cell-furniture snap exact: a glyph scaled about the |
| 31 | /// baseline by `height / exact_height` reaches the top and the bottom of |
| 32 | /// the cell precisely when the baseline sits here. |
| 33 | pub baseline: f32, |
| 34 | /// The advance before rounding, kept so the residual is measurable rather |
| 35 | /// than folklore. See [`rounding_error`](Self::rounding_error). |
| 36 | pub exact_advance: f32, |
| 37 | /// The line height before rounding. |
| 38 | pub exact_height: f32, |
| 39 | |
| 40 | |
| 41 | |
| 42 | /// Measure a face at a size. |
| 43 | /// |
| 44 | /// Takes bytes rather than a live renderer on purpose: `shop` needs the |
| 45 | /// cell before it has a wgpu device, because the PTY is sized in cells and |
| 46 | /// is spawned first. |
| 47 | |
| 48 | let font = from_index |
| 49 | .ok_or_else?; |
| 50 | let metrics = font.metrics.scale; |
| 51 | |
| 52 | // One representative glyph rather than `average_width` or `max_width`. |
| 53 | // The grid has no concept of a per-glyph width anywhere outside a |
| 54 | // cluster, so what is wanted is the advance every cell will actually |
| 55 | // use, and in a monospace face `M` carries it. `average_width` is a |
| 56 | // weighted figure that means nothing here, and `max_width` is whatever |
| 57 | // the widest glyph in the file happens to be — in a Nerd Font, a |
| 58 | // double-width icon. |
| 59 | let exact_advance = font |
| 60 | .glyph_metrics |
| 61 | .scale |
| 62 | .advance_width; |
| 63 | |
| 64 | // Ascent, descent and leading, which is the line box the face asks for. |
| 65 | // `descent` is positive in swash, so this is a sum. |
| 66 | let exact_height = metrics.ascent + metrics.descent + metrics.leading; |
| 67 | |
| 68 | Ok |
| 69 | // Rounded, because a cell has to be a whole number of pixels: a |
| 70 | // fractional advance puts every column at a fractional x, and the |
| 71 | // glyph is resampled rather than drawn. |
| 72 | // |
| 73 | // **Up, not to nearest**, for the same reason the height is: a cell |
| 74 | // narrower than the advance makes every glyph overlap its right-hand |
| 75 | // neighbour, which corrupts the per-row render cache, where a cell |
| 76 | // wider than the advance only leaves a hairline of background that |
| 77 | // the furniture snap closes anyway. |
| 78 | // |
| 79 | // This stopped being hypothetical when shop's bundled face changed. |
| 80 | // IosevkaTerm advanced 1/2 em and landed whole at every even size, |
| 81 | // so `round` never rounded down in practice. Quasi Mono advances |
| 82 | // 79/125 em and 79 is prime, so it is whole at 125px and nowhere |
| 83 | // usable — at 15px it measures 9.48 and `round` would have taken |
| 84 | // half a pixel out of every column. |
| 85 | advance: exact_advance.ceil.max, |
| 86 | // Rounded *up*, not to nearest. A cell shorter than the line clips |
| 87 | // the descenders of every row, and a glyph bleeding into the row |
| 88 | // below breaks the per-row render cache; the cost of rounding up is |
| 89 | // a sub-pixel of background between rows, which is the cheaper of |
| 90 | // the two. |
| 91 | height: exact_height.ceil.max, |
| 92 | ascent: metrics.ascent, |
| 93 | baseline: if exact_height > 0.0 |
| 94 | metrics.ascent * exact_height.ceil.max / exact_height |
| 95 | else |
| 96 | metrics.ascent |
| 97 | , |
| 98 | exact_advance, |
| 99 | exact_height, |
| 100 | |
| 101 | |
| 102 | |
| 103 | /// The same cell in physical pixels at an integer output scale. |
| 104 | /// |
| 105 | /// Every field scales, so the snap factors derived from it stay the ones |
| 106 | /// the grid is actually stepping by. That matters more than it looks: the |
| 107 | /// renderer is rebuilt at `font_px * scale`, and a face whose advance is |
| 108 | /// fractional does not measure there to exactly `advance * scale`. Snapping |
| 109 | /// to this rather than to the renderer's own measurement is what makes the |
| 110 | /// two agree instead of drifting by a fraction of a pixel. |
| 111 | |
| 112 | |
| 113 | let s = scale.max as f32; |
| 114 | Self |
| 115 | advance: self.advance * s, |
| 116 | height: self.height * s, |
| 117 | ascent: self.ascent * s, |
| 118 | baseline: self.baseline * s, |
| 119 | exact_advance: self.exact_advance * s, |
| 120 | exact_height: self.exact_height * s, |
| 121 | |
| 122 | |
| 123 | |
| 124 | /// How far the whole-pixel cell sits from the face's own metrics. |
| 125 | /// |
| 126 | /// Zero is not achievable in general and is not aimed for. An advance is |
| 127 | /// a fraction of an em, so it lands on a whole pixel only at sizes that |
| 128 | /// clear its denominator: IosevkaTerm is 1/2 em and does it at every even |
| 129 | /// size, IBM Plex Mono is 3/5 and needs a multiple of 5, and Atkinson |
| 130 | /// Hyperlegible Mono is 79/125 and does it at 125px and nowhere usable. |
| 131 | /// Chasing it would mean choosing the font size to suit the face. |
| 132 | /// |
| 133 | /// So the renderer snaps cell furniture to the cell instead, and this |
| 134 | /// number is what the snap absorbs. Kept because it says how hard the snap |
| 135 | /// is working, which is worth a glance when a face is swapped. |
| 136 | |
| 137 | |
| 138 | self.advance - self.exact_advance, |
| 139 | self.height - self.exact_height, |
| 140 | |
| 141 | |
| 142 | |
| 143 | |
| 144 | |
| 145 | |
| 146 | use *; |
| 147 | |
| 148 | const FONT: & = FACE; |
| 149 | |
| 150 | // The measurement the hardcoded constants got wrong, pinned so a font swap |
| 151 | // cannot quietly reintroduce it. |
| 152 | // |
| 153 | // Stated as the face's own tables rather than as two numbers, because the |
| 154 | // numbers moved when the bundled face did: IosevkaTerm is 1/2 em and 14px |
| 155 | // gave exactly 7.0, Quasi Mono is 79/125 and gives 8.848. A test that |
| 156 | // asserted 7.0 was asserting which font shop bundles, which is what the |
| 157 | // rest of this file is for. |
| 158 | |
| 159 | |
| 160 | let font = from_index.unwrap; |
| 161 | let m = font.metrics.scale; |
| 162 | let cell = measure.unwrap; |
| 163 | assert! |
| 164 | |
| 165 | - font |
| 166 | .glyph_metrics |
| 167 | .scale |
| 168 | .advance_width |
| 169 | .abs |
| 170 | < 0.01, |
| 171 | "advance {} is not the face's own", |
| 172 | cell.exact_advance |
| 173 | ; |
| 174 | assert! |
| 175 | .abs < 0.01, |
| 176 | "height {} is not ascent + descent + leading", |
| 177 | cell.exact_height |
| 178 | ; |
| 179 | assert!; |
| 180 | assert!; |
| 181 | |
| 182 | |
| 183 | // The old constants, as a record of what was wrong rather than as a target. |
| 184 | // |
| 185 | // They were 8.0 x 17.0 and derived from no face at all, which is the defect: |
| 186 | // against IosevkaTerm at 14px (7.0 x 17.5) every column carried a pixel of |
| 187 | // dead space and every row lost half of one. That they happen to sit closer |
| 188 | // to the face bundled today is luck and changes nothing — a number nothing |
| 189 | // measures is wrong the moment the font moves, which it has since. |
| 190 | |
| 191 | |
| 192 | let cell = measure.unwrap; |
| 193 | assert! |
| 194 | .abs > 0.5, |
| 195 | "the hardcoded 8.0 is {} off this face's advance", |
| 196 | 8.0 - cell.exact_advance |
| 197 | ; |
| 198 | assert! |
| 199 | cell.exact_height > 17.0, |
| 200 | "the hardcoded 17.0 clipped {} of every row", |
| 201 | cell.exact_height - 17.0 |
| 202 | ; |
| 203 | |
| 204 | |
| 205 | // The property the cell exists to have. Cell furniture is drawn to fill |
| 206 | // its cell exactly, so a cell wider than the advance shows background |
| 207 | // between every column: `───` gets a hole every cell and `███` draws as |
| 208 | // stripes. Measured against the face's own box-drawing glyphs rather than |
| 209 | // argued, because this is the failure the hardcoded 8.0 actually produced. |
| 210 | |
| 211 | |
| 212 | let font = from_index.unwrap; |
| 213 | let cell = measure.unwrap; |
| 214 | let glyphs = font.glyph_metrics.scale; |
| 215 | let charmap = font.charmap; |
| 216 | for c in |
| 217 | let advance = glyphs.advance_width; |
| 218 | assert! |
| 219 | .abs < 0.01, |
| 220 | "`{c}` advances {advance} in a {} cell", |
| 221 | cell.exact_advance |
| 222 | ; |
| 223 | |
| 224 | |
| 225 | |
| 226 | |
| 227 | |
| 228 | let one = measure.unwrap; |
| 229 | let two = measure.unwrap; |
| 230 | assert!; |
| 231 | assert!; |
| 232 | |
| 233 | |
| 234 | |
| 235 | |
| 236 | let cell = measure.unwrap; |
| 237 | assert!; |
| 238 | |
| 239 | |
| 240 | |
| 241 | |
| 242 | assert!; |
| 243 | |
| 244 | |
| 245 |