From 5682027a947c61a72347078812dc47bf4da05253 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 24 Jun 2026 14:01:25 +0300 Subject: [PATCH] font: render colour emoji from bitmap strikes, scaled to the cell Signed-off-by: NotAShelf Change-Id: If30e5f13ee24e691b417ad35c588a6226a6a6964 --- src/font.rs | 39 ++++++++++++++++++++++++++++++++++---- src/render.rs | 52 ++++++++++++++++++++++++++------------------------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/font.rs b/src/font.rs index 16587be..3ac11c8 100644 --- a/src/font.rs +++ b/src/font.rs @@ -213,9 +213,7 @@ impl Fonts { } let index = matched.face_index().unwrap_or(0) as isize; let face = self.library.new_face(&path, index)?; - // Bitmap-strike-only fonts reject an arbitrary pixel size; skip them - // rather than guess a strike. - if face.set_pixel_sizes(0, self.size_px).is_err() { + if size_face(&face, self.size_px).is_err() { return Ok(None); } self.faces.push(face); @@ -240,10 +238,43 @@ fn resolve_face( .find(family, Some(style.fontconfig_style())) .map_err(|_| FontError::NoFamily(family.to_owned()))?; let face = library.new_face(&font.path, font.index.unwrap_or(0) as isize)?; - face.set_pixel_sizes(0, size_px)?; + size_face(&face, size_px)?; Ok(face) } +/// Set a face to `size_px`. Scalable faces size directly; bitmap-strike faces +/// (e.g. colour-emoji fonts) cannot, so select the nearest available strike and +/// let the renderer scale its glyphs into the cell. +fn size_face(face: &Face, size_px: u32) -> Result<(), FontError> { + match face.set_pixel_sizes(0, size_px) { + Ok(()) => Ok(()), + Err(_) if face.has_fixed_sizes() => { + face.select_size(nearest_strike(face, size_px))?; + Ok(()) + } + Err(err) => Err(err.into()), + } +} + +/// Index of the fixed strike whose pixel height is closest to `target`. +fn nearest_strike(face: &Face, target: u32) -> i32 { + let rec = face.raw(); + let target = i32::try_from(target).unwrap_or(i32::MAX); + let mut best = 0; + let mut best_delta = i32::MAX; + for i in 0..rec.num_fixed_sizes { + // SAFETY: `available_sizes` points to `num_fixed_sizes` valid + // `FT_Bitmap_Size` entries for the face's lifetime; `i` is in range. + let height = i32::from(unsafe { (*rec.available_sizes.offset(i as isize)).height }); + let delta = (height - target).abs(); + if delta < best_delta { + best = i; + best_delta = delta; + } + } + best +} + fn cell_metrics(face: &Face, family: &str) -> Result { let metrics = face .size_metrics() diff --git a/src/render.rs b/src/render.rs index a449b63..9ea9bb4 100644 --- a/src/render.rs +++ b/src/render.rs @@ -154,7 +154,7 @@ impl Renderer { cell.c, cell_style(cell), origin_x, - cell_top + m.ascent as i32, + cell_top, fg, ); } @@ -194,14 +194,7 @@ impl Renderer { let cell = grid.cell(cx, cy); if cell.c != ' ' && !cell.flags.contains(Flags::WIDE_CONT) { let (_, bg) = cell_colors(cell); - self.draw_glyph( - canvas, - cell.c, - cell_style(cell), - x0, - top + m.ascent as i32, - bg, - ); + self.draw_glyph(canvas, cell.c, cell_style(cell), x0, top, bg); } } CursorShape::Underline => { @@ -217,9 +210,10 @@ impl Renderer { c: char, style: Style, origin_x: i32, - baseline: i32, + cell_top: i32, fg: Rgb, ) { + let m = self.fonts.metrics(); let glyph = match self.fonts.glyph(c, style) { Ok(glyph) => glyph, Err(err) => { @@ -227,31 +221,39 @@ impl Renderer { return; } }; - let (left, top, w, h) = ( - glyph.left, - glyph.top, - glyph.width as i32, - glyph.height as i32, - ); + let (gw, gh) = (glyph.width as i32, glyph.height as i32); match &glyph.data { GlyphData::Mask(mask) => { - for gy in 0..h { - for gx in 0..w { - let a = mask[(gy * w + gx) as usize]; + let baseline = cell_top + m.ascent as i32; + for gy in 0..gh { + for gx in 0..gw { + let a = mask[(gy * gw + gx) as usize]; if a != 0 { - canvas.blend(origin_x + left + gx, baseline - top + gy, fg, a); + canvas.blend( + origin_x + glyph.left + gx, + baseline - glyph.top + gy, + fg, + a, + ); } } } } - GlyphData::Color(bgra) => { - for gy in 0..h { - for gx in 0..w { - let i = ((gy * w + gx) * 4) as usize; - canvas.over(origin_x + left + gx, baseline - top + gy, &bgra[i..i + 4]); + // Colour glyphs (emoji) come from a fixed strike at native size; + // scale them to the line height with nearest-neighbour sampling. + GlyphData::Color(bgra) if gh > 0 => { + let scale = m.height as f32 / gh as f32; + let target_w = (gw as f32 * scale) as i32; + for ty in 0..m.height as i32 { + let sy = ((ty as f32 / scale) as i32).min(gh - 1); + for tx in 0..target_w { + let sx = ((tx as f32 / scale) as i32).min(gw - 1); + let i = ((sy * gw + sx) * 4) as usize; + canvas.over(origin_x + tx, cell_top + ty, &bgra[i..i + 4]); } } } + GlyphData::Color(_) => {} } } }