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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user