| 8 |
8 |
|
|
| 9 |
9 |
|
use std::collections::HashMap;
|
| 10 |
10 |
|
|
|
11 |
+ |
use std::collections::HashSet;
|
|
12 |
+ |
|
| 11 |
13 |
|
use swash::{
|
| 12 |
14 |
|
FontRef, GlyphId,
|
| 13 |
15 |
|
scale::{Render, ScaleContext, Source, image::Content},
|
| 14 |
16 |
|
shape::ShapeContext,
|
| 15 |
|
- |
zeno::Format,
|
|
17 |
+ |
zeno::{Format, Transform},
|
| 16 |
18 |
|
};
|
| 17 |
19 |
|
|
| 18 |
20 |
|
use crate::fallback::Fallback;
|
|
21 |
+ |
use crate::metrics::CellMetrics;
|
|
22 |
+ |
|
|
23 |
+ |
/// Characters drawn to meet their neighbours rather than to sit inside their
|
|
24 |
+ |
/// own cell.
|
|
25 |
+ |
///
|
|
26 |
+ |
/// Box drawing and block elements are the obvious ones: a border seams if the
|
|
27 |
+ |
/// bar does not reach both edges. Braille is here because a `Canvas` or a
|
|
28 |
+ |
/// sparkline paints one picture across many cells. The legacy-computing
|
|
29 |
+ |
/// sextants are blocks by another name. And the four powerline separators are
|
|
30 |
+ |
/// the classic visible case — a prompt with a hairline of background showing
|
|
31 |
+ |
/// through every chevron.
|
|
32 |
+ |
///
|
|
33 |
+ |
/// Over-including is cheap and under-including is not: a glyph that did not
|
|
34 |
+ |
/// need snapping is scaled by well under a pixel, which nobody can see, while
|
|
35 |
+ |
/// a glyph that needed it and did not get it leaves a seam on every row.
|
|
36 |
+ |
pub(crate) fn is_cell_furniture(c: char) -> bool {
|
|
37 |
+ |
matches!(c,
|
|
38 |
+ |
'\u{2500}'..='\u{259F}' // box drawing, block elements
|
|
39 |
+ |
| '\u{2800}'..='\u{28FF}' // braille
|
|
40 |
+ |
| '\u{E0B0}'..='\u{E0B3}' // powerline separators
|
|
41 |
+ |
| '\u{1FB00}'..='\u{1FBFF}' // legacy computing
|
|
42 |
+ |
)
|
|
43 |
+ |
}
|
| 19 |
44 |
|
|
| 20 |
45 |
|
/// Which font in the set. 0 is always the bundled one.
|
| 21 |
46 |
|
pub(crate) type FontId = u16;
|
| 39 |
64 |
|
scale_ctx: ScaleContext,
|
| 40 |
65 |
|
shape_ctx: ShapeContext,
|
| 41 |
66 |
|
px: f32,
|
|
67 |
+ |
/// The cell this shaper is drawing into, in physical pixels.
|
|
68 |
+ |
cell: CellMetrics,
|
|
69 |
+ |
/// Scale that maps the face's own cell onto the pixel one, applied to
|
|
70 |
+ |
/// [`is_cell_furniture`] glyphs at rasterization.
|
|
71 |
+ |
///
|
|
72 |
+ |
/// Both factors are within a pixel of 1, because the pixel cell is the
|
|
73 |
+ |
/// face's own measurements rounded. That is the whole trick: the distortion
|
|
74 |
+ |
/// is too small to see and it is exactly enough to close the seam.
|
|
75 |
+ |
snap: (f32, f32),
|
|
76 |
+ |
/// Glyph ids known to be furniture, learned as characters are resolved.
|
|
77 |
+ |
///
|
|
78 |
+ |
/// Kept by id rather than re-derived from a character, because
|
|
79 |
+ |
/// rasterization is reached with a glyph id and a font and nothing else —
|
|
80 |
+ |
/// which is also what the atlas cache is keyed on, so a glyph is snapped or
|
|
81 |
+ |
/// not for its whole life and never both.
|
|
82 |
+ |
furniture: HashSet<(FontId, GlyphId)>,
|
| 42 |
83 |
|
}
|
| 43 |
84 |
|
|
| 44 |
85 |
|
pub(crate) struct ShapedGlyph {
|
| 61 |
102 |
|
}
|
| 62 |
103 |
|
|
| 63 |
104 |
|
impl Shaper {
|
| 64 |
|
- |
pub(crate) fn new(font_data: Vec<u8>, px: f32) -> anyhow::Result<Self> {
|
|
105 |
+ |
pub(crate) fn new(font_data: Vec<u8>, px: f32, cell: CellMetrics) -> anyhow::Result<Self> {
|
| 65 |
106 |
|
let font = FontRef::from_index(&font_data, 0)
|
| 66 |
107 |
|
.ok_or_else(|| anyhow::anyhow!("swash: not a font"))?;
|
| 67 |
108 |
|
let font_offset = font.offset;
|
|
109 |
+ |
// Measured here rather than taken from `cell`, and the difference is
|
|
110 |
+ |
// the point: `cell` is the grid's step in physical pixels, which is a
|
|
111 |
+ |
// logical cell times an integer scale, while this is what the face
|
|
112 |
+ |
// draws at the size this shaper rasterizes at. Snapping between the two
|
|
113 |
+ |
// absorbs the gap instead of leaving it on screen.
|
|
114 |
+ |
let own = CellMetrics::measure(&font_data, px)?;
|
|
115 |
+ |
let snap = (
|
|
116 |
+ |
safe_ratio(cell.advance, own.exact_advance),
|
|
117 |
+ |
safe_ratio(cell.height, own.exact_height),
|
|
118 |
+ |
);
|
| 68 |
119 |
|
Ok(Self {
|
| 69 |
120 |
|
faces: vec![Face {
|
| 70 |
121 |
|
data: font_data,
|
| 76 |
127 |
|
scale_ctx: ScaleContext::new(),
|
| 77 |
128 |
|
shape_ctx: ShapeContext::new(),
|
| 78 |
129 |
|
px,
|
|
130 |
+ |
cell,
|
|
131 |
+ |
snap,
|
|
132 |
+ |
furniture: HashSet::new(),
|
| 79 |
133 |
|
})
|
| 80 |
134 |
|
}
|
| 81 |
135 |
|
|
| 97 |
151 |
|
pub(crate) fn glyph_id_for(&mut self, c: char) -> (FontId, GlyphId) {
|
| 98 |
152 |
|
let id = self.face(PRIMARY).charmap().map(c);
|
| 99 |
153 |
|
if id != 0 {
|
|
154 |
+ |
if is_cell_furniture(c) {
|
|
155 |
+ |
self.furniture.insert((PRIMARY, id));
|
|
156 |
+ |
}
|
| 100 |
157 |
|
return (PRIMARY, id);
|
| 101 |
158 |
|
}
|
| 102 |
159 |
|
match self.resolve(c) {
|
| 103 |
160 |
|
Some(font) => {
|
| 104 |
161 |
|
let id = self.face(font).charmap().map(c);
|
| 105 |
|
- |
if id == 0 { (PRIMARY, 0) } else { (font, id) }
|
|
162 |
+ |
if id == 0 {
|
|
163 |
+ |
(PRIMARY, 0)
|
|
164 |
+ |
} else {
|
|
165 |
+ |
if is_cell_furniture(c) {
|
|
166 |
+ |
self.furniture.insert((font, id));
|
|
167 |
+ |
}
|
|
168 |
+ |
(font, id)
|
|
169 |
+ |
}
|
| 106 |
170 |
|
}
|
| 107 |
171 |
|
None => (PRIMARY, 0),
|
| 108 |
172 |
|
}
|
| 173 |
237 |
|
.size(self.px)
|
| 174 |
238 |
|
.hint(true)
|
| 175 |
239 |
|
.build();
|
| 176 |
|
- |
let image = Render::new(&[Source::Outline])
|
| 177 |
|
- |
.format(Format::Alpha)
|
| 178 |
|
- |
.render(&mut scaler, id)?;
|
|
240 |
+ |
let mut render = Render::new(&[Source::Outline]);
|
|
241 |
+ |
render.format(Format::Alpha);
|
|
242 |
+ |
// Furniture is drawn to the cell, not to its own advance. Scaled about
|
|
243 |
+ |
// the pen origin, which sits on the baseline at the cell's left edge,
|
|
244 |
+ |
// so the horizontal factor takes the bar out to both edges and the
|
|
245 |
+ |
// vertical one takes it to the top and bottom — given a baseline at
|
|
246 |
+ |
// `CellMetrics::baseline`, which is what `ascent` below reports.
|
|
247 |
+ |
if self.furniture.contains(&(font, id)) {
|
|
248 |
+ |
render.transform(Some(Transform::scale(self.snap.0, self.snap.1)));
|
|
249 |
+ |
}
|
|
250 |
+ |
let image = render.render(&mut scaler, id)?;
|
| 179 |
251 |
|
if image.content != Content::Mask {
|
| 180 |
252 |
|
return None;
|
| 181 |
253 |
|
}
|
| 188 |
260 |
|
})
|
| 189 |
261 |
|
}
|
| 190 |
262 |
|
|
| 191 |
|
- |
/// The baseline, taken from the bundled font only.
|
|
263 |
+ |
/// The baseline, as a distance down from the top of the cell.
|
|
264 |
+ |
///
|
|
265 |
+ |
/// Named for what it is rather than for the metric it used to be. It
|
|
266 |
+ |
/// returned the face's raw ascent until the cell furniture was snapped,
|
|
267 |
+ |
/// and the two are no longer the same number.
|
| 192 |
268 |
|
///
|
| 193 |
269 |
|
/// One baseline for the whole grid, whatever font a given cell came from:
|
| 194 |
270 |
|
/// a fallback face with its own ascent would sit its glyphs on a different
|
| 195 |
271 |
|
/// line and make a row of mixed scripts wander.
|
| 196 |
|
- |
pub(crate) fn ascent(&self) -> f32 {
|
| 197 |
|
- |
self.face(PRIMARY).metrics(&[]).scale(self.px).ascent
|
|
272 |
+ |
///
|
|
273 |
+ |
/// The cell's baseline rather than the face's raw ascent. The cell is the
|
|
274 |
+ |
/// line height rounded up, and putting the baseline proportionally inside
|
|
275 |
+ |
/// it shares that slack the way the face shares it rather than dropping all
|
|
276 |
+ |
/// of it under the descender. It is also the position the furniture snap is
|
|
277 |
+ |
/// derived against, so a full block reaches both edges of the cell exactly.
|
|
278 |
+ |
pub(crate) fn baseline(&self) -> f32 {
|
|
279 |
+ |
self.cell.baseline
|
|
280 |
+ |
}
|
|
281 |
+ |
}
|
|
282 |
+ |
|
|
283 |
+ |
/// `a / b`, or 1.0 when `b` is not a usable divisor.
|
|
284 |
+ |
///
|
|
285 |
+ |
/// A face that measured to nothing would otherwise turn every glyph into a
|
|
286 |
+ |
/// division by zero; drawing it unsnapped is the harmless answer.
|
|
287 |
+ |
fn safe_ratio(a: f32, b: f32) -> f32 {
|
|
288 |
+ |
if b > f32::EPSILON { a / b } else { 1.0 }
|
|
289 |
+ |
}
|
|
290 |
+ |
|
|
291 |
+ |
#[cfg(test)]
|
|
292 |
+ |
mod tests {
|
|
293 |
+ |
use super::*;
|
|
294 |
+ |
|
|
295 |
+ |
const FONT: &[u8] = include_bytes!("../../../assets/IosevkaTermNerdFontMono-Regular.ttf");
|
|
296 |
+ |
|
|
297 |
+ |
/// A shaper over the bundled face, drawing into the cell that face implies.
|
|
298 |
+ |
fn shaper(px: f32, cell: CellMetrics) -> Shaper {
|
|
299 |
+ |
Shaper::new(FONT.to_vec(), px, cell).expect("the bundled face loads")
|
|
300 |
+ |
}
|
|
301 |
+ |
|
|
302 |
+ |
/// The rasterized extent of `c`, as (left, right, top, bottom) in pixels
|
|
303 |
+ |
/// relative to the cell's top-left corner.
|
|
304 |
+ |
fn extent(shaper: &mut Shaper, c: char) -> (f32, f32, f32, f32) {
|
|
305 |
+ |
let (font, id) = shaper.glyph_id_for(c);
|
|
306 |
+ |
let baseline = shaper.baseline();
|
|
307 |
+ |
let r = shaper.rasterize(font, id).expect("the glyph rasterizes");
|
|
308 |
+ |
let left = r.placement_left as f32;
|
|
309 |
+ |
let top = baseline - r.placement_top as f32;
|
|
310 |
+ |
(left, left + r.width as f32, top, top + r.height as f32)
|
|
311 |
+ |
}
|
|
312 |
+ |
|
|
313 |
+ |
// The whole point. A full block has to ink the entire cell, or every row
|
|
314 |
+ |
// and every column of a filled region shows a hairline of background.
|
|
315 |
+ |
#[test]
|
|
316 |
+ |
fn a_full_block_inks_the_whole_cell() {
|
|
317 |
+ |
let cell = CellMetrics::measure(FONT, 14.0).unwrap();
|
|
318 |
+ |
let mut shaper = shaper(14.0, cell);
|
|
319 |
+ |
let (x0, x1, y0, y1) = extent(&mut shaper, '█');
|
|
320 |
+ |
assert!(x0 <= 0.5, "left edge at {x0}");
|
|
321 |
+ |
assert!(
|
|
322 |
+ |
x1 >= cell.advance - 0.5,
|
|
323 |
+ |
"right edge at {x1} of {}",
|
|
324 |
+ |
cell.advance
|
|
325 |
+ |
);
|
|
326 |
+ |
assert!(y0 <= 0.5, "top edge at {y0}");
|
|
327 |
+ |
assert!(
|
|
328 |
+ |
y1 >= cell.height - 0.5,
|
|
329 |
+ |
"bottom edge at {y1} of {}",
|
|
330 |
+ |
cell.height
|
|
331 |
+ |
);
|
|
332 |
+ |
}
|
|
333 |
+ |
|
|
334 |
+ |
// The snap is doing real work and almost none of it, which is what makes
|
|
335 |
+ |
// it safe. The block ends up taller than the face's own line box, by the
|
|
336 |
+ |
// half pixel the cell rounding added, and the factor that did it is well
|
|
337 |
+ |
// inside a pixel — so nothing on screen is visibly distorted.
|
|
338 |
+ |
#[test]
|
|
339 |
+ |
fn the_snap_closes_the_rounding_and_nothing_more() {
|
|
340 |
+ |
let cell = CellMetrics::measure(FONT, 14.0).unwrap();
|
|
341 |
+ |
let mut shaper = shaper(14.0, cell);
|
|
342 |
+ |
let (sx, sy) = shaper.snap;
|
|
343 |
+ |
assert!(
|
|
344 |
+ |
(sx - 1.0).abs() < 0.05 && (sy - 1.0).abs() < 0.05,
|
|
345 |
+ |
"the snap is stretching by ({sx}, {sy}), which would be visible"
|
|
346 |
+ |
);
|
|
347 |
+ |
let (_, _, y0, y1) = extent(&mut shaper, '\u{2588}');
|
|
348 |
+ |
let inked = y1 - y0;
|
|
349 |
+ |
assert!(
|
|
350 |
+ |
inked > cell.exact_height,
|
|
351 |
+ |
"the block inks {inked}, which is no more than the face's own {} \
|
|
352 |
+ |
line box — so the cell's extra {} is still background",
|
|
353 |
+ |
cell.exact_height,
|
|
354 |
+ |
cell.height - cell.exact_height
|
|
355 |
+ |
);
|
|
356 |
+ |
}
|
|
357 |
+ |
|
|
358 |
+ |
// A rule has to reach both edges, or a horizontal border is a dashed line.
|
|
359 |
+ |
#[test]
|
|
360 |
+ |
fn a_horizontal_rule_reaches_both_edges_of_the_cell() {
|
|
361 |
+ |
let cell = CellMetrics::measure(FONT, 14.0).unwrap();
|
|
362 |
+ |
let mut shaper = shaper(14.0, cell);
|
|
363 |
+ |
let (x0, x1, _, _) = extent(&mut shaper, '─');
|
|
364 |
+ |
assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}");
|
|
365 |
+ |
}
|
|
366 |
+ |
|
|
367 |
+ |
// And a vertical one has to reach the top and the bottom, which is the
|
|
368 |
+ |
// half-pixel the cell rounding created.
|
|
369 |
+ |
#[test]
|
|
370 |
+ |
fn a_vertical_rule_reaches_the_top_and_bottom_of_the_cell() {
|
|
371 |
+ |
let cell = CellMetrics::measure(FONT, 14.0).unwrap();
|
|
372 |
+ |
let mut shaper = shaper(14.0, cell);
|
|
373 |
+ |
let (_, _, y0, y1) = extent(&mut shaper, '│');
|
|
374 |
+ |
assert!(y0 <= 0.5, "top at {y0}");
|
|
375 |
+ |
assert!(y1 >= cell.height - 0.5, "bottom at {y1} of {}", cell.height);
|
|
376 |
+ |
}
|
|
377 |
+ |
|
|
378 |
+ |
// Text is not furniture and must not be stretched: a letter scaled to the
|
|
379 |
+ |
// cell would be a different typeface.
|
|
380 |
+ |
#[test]
|
|
381 |
+ |
fn a_letter_is_left_alone() {
|
|
382 |
+ |
let cell = CellMetrics::measure(FONT, 14.0).unwrap();
|
|
383 |
+ |
let mut shaper = shaper(14.0, cell);
|
|
384 |
+ |
let (_, _, y0, y1) = extent(&mut shaper, 'x');
|
|
385 |
+ |
assert!(y0 > 1.0 && y1 < cell.height - 1.0, "`x` spans {y0}..{y1}");
|
|
386 |
+ |
}
|
|
387 |
+ |
|
|
388 |
+ |
// The case the snap was built for. At 15px the bundled face advances 7.5,
|
|
389 |
+ |
// so the cell rounds to 8 and a full pixel of every column would be
|
|
390 |
+ |
// background — the same shape of defect as the hardcoded 8.0, arrived at
|
|
391 |
+ |
// honestly. An advance is a fraction of an em and lands whole only at sizes
|
|
392 |
+ |
// that clear its denominator, so this is the general case and 14px is the
|
|
393 |
+ |
// lucky one.
|
|
394 |
+ |
#[test]
|
|
395 |
+ |
fn a_size_where_the_advance_is_fractional_still_tiles() {
|
|
396 |
+ |
let cell = CellMetrics::measure(FONT, 15.0).unwrap();
|
|
397 |
+ |
assert!(
|
|
398 |
+ |
(cell.exact_advance - 7.5).abs() < 0.01,
|
|
399 |
+ |
"advance {}",
|
|
400 |
+ |
cell.exact_advance
|
|
401 |
+ |
);
|
|
402 |
+ |
assert!((cell.advance - 8.0).abs() < f32::EPSILON);
|
|
403 |
+ |
|
|
404 |
+ |
let mut shaper = shaper(15.0, cell);
|
|
405 |
+ |
let (x0, x1, y0, y1) = extent(&mut shaper, '\u{2588}');
|
|
406 |
+ |
assert!(x0 <= 0.5 && x1 >= cell.advance - 0.5, "spans {x0}..{x1}");
|
|
407 |
+ |
assert!(y0 <= 0.5 && y1 >= cell.height - 0.5, "spans {y0}..{y1}");
|
|
408 |
+ |
}
|
|
409 |
+ |
|
|
410 |
+ |
#[test]
|
|
411 |
+ |
fn the_ranges_that_snap_are_the_ones_that_meet_their_neighbours() {
|
|
412 |
+ |
for c in ['─', '┼', '╬', '█', '▄', '░', '⠿', '\u{E0B0}', '\u{1FB00}'] {
|
|
413 |
+ |
assert!(is_cell_furniture(c), "{c} should snap");
|
|
414 |
+ |
}
|
|
415 |
+ |
for c in ['a', 'M', ' ', '★', '→', '✘'] {
|
|
416 |
+ |
assert!(!is_cell_furniture(c), "{c} should not snap");
|
|
417 |
+ |
}
|
| 198 |
418 |
|
}
|
| 199 |
419 |
|
}
|