1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 12:50:04 -04:00

Subpixel glyph positioning

This commit is contained in:
valadaptive
2025-09-08 05:22:29 -04:00
parent 03a4f2c181
commit d659df6655
2 changed files with 130 additions and 37 deletions

View File

@@ -57,6 +57,67 @@ impl GlyphInfo {
};
}
// Subpixel binning, taken from cosmic-text:
// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
/// Bin for subpixel positioning of glyphs.
///
/// For accurate glyph positioning, we want to render each glyph at a subpixel coordinate. However, we also want to
/// cache each glyph's bitmap. As a compromise, we bin each subpixel offset into one of four fractional values. This
/// means one glyph can have up to four subpixel-positioned bitmaps in the cache.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(super) enum SubpixelBin {
#[default]
Zero,
One,
Two,
Three,
}
impl SubpixelBin {
/// Bin the given position and return the new integral coordinate.
fn new(pos: f32) -> (i32, Self) {
let trunc = pos as i32;
let fract = pos - trunc as f32;
#[expect(clippy::collapsible_else_if)]
if pos.is_sign_negative() {
if fract > -0.125 {
(trunc, Self::Zero)
} else if fract > -0.375 {
(trunc - 1, Self::Three)
} else if fract > -0.625 {
(trunc - 1, Self::Two)
} else if fract > -0.875 {
(trunc - 1, Self::One)
} else {
(trunc - 1, Self::Zero)
}
} else {
if fract < 0.125 {
(trunc, Self::Zero)
} else if fract < 0.375 {
(trunc, Self::One)
} else if fract < 0.625 {
(trunc, Self::Two)
} else if fract < 0.875 {
(trunc, Self::Three)
} else {
(trunc + 1, Self::Zero)
}
}
}
pub fn as_float(&self) -> f32 {
match self {
Self::Zero => 0.0,
Self::One => 0.25,
Self::Two => 0.5,
Self::Three => 0.75,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GlyphAllocation {
/// Used for pair-kerning.
@@ -65,7 +126,7 @@ pub struct GlyphAllocation {
/// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
pub(crate) id: ab_glyph::GlyphId,
/// Unit: points.
/// Unit: screen pixels.
pub advance_width: f32,
/// UV rectangle for drawing.
@@ -81,7 +142,7 @@ pub struct FontImpl {
ab_glyph_font: ab_glyph::FontArc,
tweak: FontTweak,
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
glyph_alloc_cache: ahash::HashMap<(ab_glyph::GlyphId, u64), GlyphAllocation>,
glyph_alloc_cache: ahash::HashMap<(ab_glyph::GlyphId, SubpixelBin, u64), GlyphAllocation>,
}
trait FontExt {
@@ -203,14 +264,23 @@ impl FontImpl {
}
#[inline]
pub fn pair_kerning(
pub(super) fn pair_kerning_screen_space(
&self,
metrics: &ScaledMetrics,
last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId,
) -> f32 {
self.ab_glyph_font.kern_unscaled(last_glyph_id, glyph_id) * metrics.px_scale_factor
/ metrics.pixels_per_point
}
#[inline]
pub fn pair_kerning(
&self,
metrics: &ScaledMetrics,
last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId,
) -> f32 {
self.pair_kerning_screen_space(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
}
#[inline(always)]
@@ -243,18 +313,21 @@ impl FontImpl {
atlas: &mut TextureAtlas,
metrics: &ScaledMetrics,
glyph_info: GlyphInfo,
) -> GlyphAllocation {
h_pos: f32,
) -> (GlyphAllocation, i32) {
let Some(glyph_id) = glyph_info.id else {
// Invisible.
return GlyphAllocation::default();
return (GlyphAllocation::default(), h_pos as i32);
};
let (h_pos_round, bin) = SubpixelBin::new(h_pos);
let entry = match self
.glyph_alloc_cache
.entry((glyph_id, metrics.glyph_cache_key()))
.entry((glyph_id, bin, metrics.glyph_cache_key()))
{
std::collections::hash_map::Entry::Occupied(glyph_alloc) => {
return *glyph_alloc.get();
return (*glyph_alloc.get(), h_pos_round);
}
std::collections::hash_map::Entry::Vacant(entry) => entry,
};
@@ -268,7 +341,10 @@ impl FontImpl {
// (https://github.com/alexheretic/ab-glyph/issues/15), and this field is never accessed when
// rasterizing. We can just put anything here.
scale: PxScale::from(0.0),
position: ab_glyph::Point::default(),
position: ab_glyph::Point {
x: bin.as_float(),
y: 0.0,
},
};
let outlined = OutlinedGlyph::new(
glyph,
@@ -315,12 +391,11 @@ impl FontImpl {
let allocation = GlyphAllocation {
id: glyph_id,
advance_width: (glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor)
/ metrics.pixels_per_point,
advance_width: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
uv_rect,
};
entry.insert(allocation);
allocation
(allocation, h_pos_round)
}
}

View File

@@ -45,7 +45,7 @@ impl PointScale {
/// Temporary storage before line-wrapping.
#[derive(Clone)]
struct Paragraph {
/// Start of the next glyph to be added.
/// Start of the next glyph to be added. In screen-space / physical pixels.
pub cursor_x: f32,
/// This is included in case there are no glyphs
@@ -169,7 +169,7 @@ fn layout_section(
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
}
paragraph.cursor_x += leading_space;
paragraph.cursor_x += leading_space * pixels_per_point;
let mut last_glyph_id = None;
@@ -195,23 +195,32 @@ fn layout_section(
(font_impl, scaled_metrics.unwrap_or_default())
}
};
let glyph_alloc = match font_impl.as_mut() {
Some(font_impl) => {
font_impl.allocate_glyph(font.atlas, &font_impl_metrics, glyph_info)
}
if let (Some(font_impl), Some(last_glyph_id), Some(glyph_id)) =
(&font_impl, last_glyph_id, glyph_info.id)
{
paragraph.cursor_x += font_impl.pair_kerning_screen_space(
&font_impl_metrics,
last_glyph_id,
glyph_id,
);
paragraph.cursor_x += extra_letter_spacing * pixels_per_point;
}
let (glyph_alloc, physical_x) = match font_impl.as_mut() {
Some(font_impl) => font_impl.allocate_glyph(
font.atlas,
&font_impl_metrics,
glyph_info,
paragraph.cursor_x,
),
None => Default::default(),
};
if let (Some(font_impl), Some(last_glyph_id)) = (&font_impl, last_glyph_id) {
paragraph.cursor_x +=
font_impl.pair_kerning(&font_impl_metrics, last_glyph_id, glyph_alloc.id);
paragraph.cursor_x += extra_letter_spacing;
}
paragraph.glyphs.push(Glyph {
chr,
pos: pos2(paragraph.cursor_x, f32::NAN),
advance_width: glyph_alloc.advance_width,
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
advance_width: glyph_alloc.advance_width / pixels_per_point,
line_height,
font_impl_height: font_impl_metrics.row_height,
font_impl_ascent: font_impl_metrics.ascent,
@@ -222,7 +231,6 @@ fn layout_section(
});
paragraph.cursor_x += glyph_alloc.advance_width;
paragraph.cursor_x = paragraph.cursor_x.round_to_pixels(pixels_per_point);
last_glyph_id = Some(glyph_alloc.id);
}
}
@@ -454,14 +462,6 @@ fn replace_last_glyph_with_overflow_character(
let (font_id, glyph_info) = font.glyph_info(overflow_character);
let mut font_impl = font.fonts_by_id.get_mut(&font_id);
let font_impl_metrics = font_impl
.as_ref()
.map(|f| f.scaled_metrics(pixels_per_point, font_size))
.unwrap_or_default();
let replacement_glyph_alloc = font_impl
.as_mut()
.map(|f| f.allocate_glyph(font.atlas, &font_impl_metrics, glyph_info))
.unwrap_or_default();
let font_impl_metrics = font_impl
.as_mut()
.map(|f| f.scaled_metrics(pixels_per_point, font_size))
@@ -488,12 +488,30 @@ fn replace_last_glyph_with_overflow_character(
0.0 // TODO(emilk): heed paragraph leading_space 😬
};
let replacement_glyph_width = font_impl
.as_mut()
.and_then(|f| f.glyph_info(overflow_character))
.map(|i| i.advance_width_unscaled.0 * font_impl_metrics.px_scale_factor)
.unwrap_or_default();
// Check if we're within width budget:
if overflow_glyph_x + replacement_glyph_alloc.advance_width <= job.effective_wrap_width()
if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
|| row.glyphs.is_empty()
{
// we are done
let (replacement_glyph_alloc, physical_x) = font_impl
.as_mut()
.map(|f| {
f.allocate_glyph(
font.atlas,
&font_impl_metrics,
glyph_info,
overflow_glyph_x * pixels_per_point,
)
})
.unwrap_or_default();
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let line_height = section
.format
@@ -502,7 +520,7 @@ fn replace_last_glyph_with_overflow_character(
row.glyphs.push(Glyph {
chr: overflow_character,
pos: pos2(overflow_glyph_x, f32::NAN),
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
advance_width: replacement_glyph_alloc.advance_width,
line_height,
font_impl_height: font_impl_metrics.row_height,