mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00: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)]
|
#[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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user