1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 14:50:03 -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)] #[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GlyphAllocation { pub struct GlyphAllocation {
/// Used for pair-kerning. /// 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. /// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
pub(crate) id: ab_glyph::GlyphId, pub(crate) id: ab_glyph::GlyphId,
/// Unit: points. /// Unit: screen pixels.
pub advance_width: f32, pub advance_width: f32,
/// UV rectangle for drawing. /// UV rectangle for drawing.
@@ -81,7 +142,7 @@ pub struct FontImpl {
ab_glyph_font: ab_glyph::FontArc, ab_glyph_font: ab_glyph::FontArc,
tweak: FontTweak, tweak: FontTweak,
glyph_info_cache: ahash::HashMap<char, GlyphInfo>, 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 { trait FontExt {
@@ -203,14 +264,23 @@ impl FontImpl {
} }
#[inline] #[inline]
pub fn pair_kerning( pub(super) fn pair_kerning_screen_space(
&self, &self,
metrics: &ScaledMetrics, metrics: &ScaledMetrics,
last_glyph_id: ab_glyph::GlyphId, last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId, glyph_id: ab_glyph::GlyphId,
) -> f32 { ) -> f32 {
self.ab_glyph_font.kern_unscaled(last_glyph_id, glyph_id) * metrics.px_scale_factor 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)] #[inline(always)]
@@ -243,18 +313,21 @@ impl FontImpl {
atlas: &mut TextureAtlas, atlas: &mut TextureAtlas,
metrics: &ScaledMetrics, metrics: &ScaledMetrics,
glyph_info: GlyphInfo, glyph_info: GlyphInfo,
) -> GlyphAllocation { h_pos: f32,
) -> (GlyphAllocation, i32) {
let Some(glyph_id) = glyph_info.id else { let Some(glyph_id) = glyph_info.id else {
// Invisible. // Invisible.
return GlyphAllocation::default(); return (GlyphAllocation::default(), h_pos as i32);
}; };
let (h_pos_round, bin) = SubpixelBin::new(h_pos);
let entry = match self let entry = match self
.glyph_alloc_cache .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) => { 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, 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 // (https://github.com/alexheretic/ab-glyph/issues/15), and this field is never accessed when
// rasterizing. We can just put anything here. // rasterizing. We can just put anything here.
scale: PxScale::from(0.0), 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( let outlined = OutlinedGlyph::new(
glyph, glyph,
@@ -315,12 +391,11 @@ impl FontImpl {
let allocation = GlyphAllocation { let allocation = GlyphAllocation {
id: glyph_id, id: glyph_id,
advance_width: (glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor) advance_width: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
/ metrics.pixels_per_point,
uv_rect, uv_rect,
}; };
entry.insert(allocation); entry.insert(allocation);
allocation (allocation, h_pos_round)
} }
} }

View File

@@ -45,7 +45,7 @@ impl PointScale {
/// Temporary storage before line-wrapping. /// Temporary storage before line-wrapping.
#[derive(Clone)] #[derive(Clone)]
struct Paragraph { 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, pub cursor_x: f32,
/// This is included in case there are no glyphs /// 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.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; let mut last_glyph_id = None;
@@ -195,23 +195,32 @@ fn layout_section(
(font_impl, scaled_metrics.unwrap_or_default()) (font_impl, scaled_metrics.unwrap_or_default())
} }
}; };
let glyph_alloc = match font_impl.as_mut() {
Some(font_impl) => { if let (Some(font_impl), Some(last_glyph_id), Some(glyph_id)) =
font_impl.allocate_glyph(font.atlas, &font_impl_metrics, glyph_info) (&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(), 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 { paragraph.glyphs.push(Glyph {
chr, chr,
pos: pos2(paragraph.cursor_x, f32::NAN), pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
advance_width: glyph_alloc.advance_width, advance_width: glyph_alloc.advance_width / pixels_per_point,
line_height, line_height,
font_impl_height: font_impl_metrics.row_height, font_impl_height: font_impl_metrics.row_height,
font_impl_ascent: font_impl_metrics.ascent, font_impl_ascent: font_impl_metrics.ascent,
@@ -222,7 +231,6 @@ fn layout_section(
}); });
paragraph.cursor_x += glyph_alloc.advance_width; 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); 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 (font_id, glyph_info) = font.glyph_info(overflow_character);
let mut font_impl = font.fonts_by_id.get_mut(&font_id); 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 let font_impl_metrics = font_impl
.as_mut() .as_mut()
.map(|f| f.scaled_metrics(pixels_per_point, font_size)) .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 😬 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: // 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() || row.glyphs.is_empty()
{ {
// we are done // 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 font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let line_height = section let line_height = section
.format .format
@@ -502,7 +520,7 @@ fn replace_last_glyph_with_overflow_character(
row.glyphs.push(Glyph { row.glyphs.push(Glyph {
chr: overflow_character, 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, advance_width: replacement_glyph_alloc.advance_width,
line_height, line_height,
font_impl_height: font_impl_metrics.row_height, font_impl_height: font_impl_metrics.row_height,