mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Replace ab_glyph with Skrifa + vello_cpu; enable font hinting (#7694)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> * Closes N/A * [x] I have followed the instructions in the PR template I'll probably come back to this and clean it up a bit. This PR reimplements ab_glyph's functionality on top of Skrifa, a somewhat lower-level font API that's being used in Chrome now. Skrifa doesn't perform rasterization itself, so I'm using [vello_cpu](https://github.com/linebender/vello) from the Linebender project for rasterization. It's still in its early days, but I believe it's already quite fast. It also supports color and gradient fills, so color emoji support will be easier. Skrifa also supports font hinting, which should make text look a bit nicer / less blurry. Here's the current ab_glyph rendering: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/2385b66e-23f8-4c6e-b8c2-ea90e0eea4e4" /> Here's Skrifa *without* hinting--it looks almost identical, but there are some subpixel differences, probably due to rasterizer behavior: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/a815f3e9-65ac-4940-bc00-571177bef53d" /> Here's Skrifa *with* hinting: <img width="1592" height="1068" alt="image" src="https://github.com/user-attachments/assets/d6cc0669-3537-4377-bba9-ed5ef09664db" /> Hinting does make the horizontal strokes look a bit bolder, which makes me wonder once again about increasing the font weight from "light" to "regular". --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
@@ -62,7 +62,7 @@ pub use self::{
|
||||
stats::PaintStats,
|
||||
stroke::{PathStroke, Stroke, StrokeKind},
|
||||
tessellator::{TessellationOptions, Tessellator},
|
||||
text::{FontFamily, FontId, Fonts, FontsView, Galley},
|
||||
text::{FontFamily, FontId, Fonts, FontsView, Galley, TextOptions},
|
||||
texture_atlas::TextureAtlas,
|
||||
texture_handle::TextureHandle,
|
||||
textures::TextureManager,
|
||||
|
||||
@@ -185,11 +185,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn text_bounding_box_under_rotation() {
|
||||
let mut fonts = Fonts::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = Fonts::new(TextOptions::default(), FontDefinitions::default());
|
||||
let font = FontId::monospace(12.0);
|
||||
|
||||
let mut t = crate::Shape::text(
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#![allow(clippy::mem_forget)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ab_glyph::{Font as _, OutlinedGlyph, PxScale};
|
||||
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
|
||||
use self_cell::self_cell;
|
||||
use skrifa::{
|
||||
MetadataProvider as _,
|
||||
raw::{TableProvider as _, tables::kern::SubtableKind},
|
||||
};
|
||||
use vello_cpu::{color, kurbo};
|
||||
|
||||
use crate::{
|
||||
TextureAtlas,
|
||||
TextOptions, TextureAtlas,
|
||||
text::{
|
||||
FontTweak,
|
||||
fonts::{CachedFamily, FontFaceKey},
|
||||
fonts::{Blob, CachedFamily, FontFaceKey},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,9 +50,9 @@ pub struct GlyphInfo {
|
||||
/// Doesn't need to be unique.
|
||||
///
|
||||
/// Is `None` for a special "invisible" glyph.
|
||||
pub(crate) id: Option<ab_glyph::GlyphId>,
|
||||
pub(crate) id: Option<skrifa::GlyphId>,
|
||||
|
||||
/// In [`ab_glyph`]s "unscaled" coordinate system.
|
||||
/// In [`skrifa`]s "unscaled" coordinate system.
|
||||
pub advance_width_unscaled: OrderedFloat<f32>,
|
||||
}
|
||||
|
||||
@@ -123,8 +130,8 @@ pub struct GlyphAllocation {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
/// Use `ab_glyph::GlyphId(0)` if you just want to have an id, and don't care.
|
||||
pub(crate) id: ab_glyph::GlyphId,
|
||||
/// Use [`skrifa::GlyphId::NOTDEF`] if you just want to have an id, and don't care.
|
||||
pub(crate) id: skrifa::GlyphId,
|
||||
|
||||
/// Unit: screen pixels.
|
||||
pub advance_width_px: f32,
|
||||
@@ -139,7 +146,7 @@ struct GlyphCacheKey(u64);
|
||||
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
|
||||
|
||||
impl GlyphCacheKey {
|
||||
fn new(glyph_id: ab_glyph::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
|
||||
fn new(glyph_id: skrifa::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
|
||||
let ScaledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
@@ -164,41 +171,229 @@ impl GlyphCacheKey {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct DependentFontData<'a> {
|
||||
skrifa: skrifa::FontRef<'a>,
|
||||
charmap: skrifa::charmap::Charmap<'a>,
|
||||
outline_glyphs: skrifa::outline::OutlineGlyphCollection<'a>,
|
||||
metrics: skrifa::metrics::Metrics,
|
||||
glyph_metrics: skrifa::metrics::GlyphMetrics<'a>,
|
||||
hinting_instance: Option<skrifa::outline::HintingInstance>,
|
||||
}
|
||||
|
||||
self_cell! {
|
||||
struct FontCell {
|
||||
owner: Blob,
|
||||
|
||||
#[covariant]
|
||||
dependent: DependentFontData,
|
||||
}
|
||||
}
|
||||
|
||||
impl FontCell {
|
||||
fn px_scale_factor(&self, scale: f32) -> f32 {
|
||||
let units_per_em = self.borrow_dependent().metrics.units_per_em as f32;
|
||||
scale / units_per_em
|
||||
}
|
||||
|
||||
fn allocate_glyph_uncached(
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &ScaledMetrics,
|
||||
glyph_info: &GlyphInfo,
|
||||
bin: SubpixelBin,
|
||||
) -> Option<GlyphAllocation> {
|
||||
let glyph_id = glyph_info.id?;
|
||||
|
||||
debug_assert!(
|
||||
glyph_id != skrifa::GlyphId::NOTDEF,
|
||||
"Can't allocate glyph for id 0"
|
||||
);
|
||||
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let mut pen = VelloPen {
|
||||
path: &mut path,
|
||||
x_offset: bin.as_float() as f64,
|
||||
};
|
||||
|
||||
self.with_dependent_mut(|_, font_data| {
|
||||
let outline = font_data.outline_glyphs.get(glyph_id)?;
|
||||
|
||||
if let Some(hinting_instance) = &mut font_data.hinting_instance {
|
||||
let size = skrifa::instance::Size::new(metrics.scale);
|
||||
if hinting_instance.size() != size {
|
||||
hinting_instance
|
||||
.reconfigure(
|
||||
&font_data.outline_glyphs,
|
||||
size,
|
||||
skrifa::instance::LocationRef::default(),
|
||||
skrifa::outline::Target::Smooth {
|
||||
mode: skrifa::outline::SmoothMode::Normal,
|
||||
symmetric_rendering: true,
|
||||
preserve_linear_metrics: true,
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
}
|
||||
let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
|
||||
outline.draw(draw_settings, &mut pen).ok()?;
|
||||
} else {
|
||||
let draw_settings = skrifa::outline::DrawSettings::unhinted(
|
||||
skrifa::instance::Size::new(metrics.scale),
|
||||
skrifa::instance::LocationRef::default(),
|
||||
);
|
||||
outline.draw(draw_settings, &mut pen).ok()?;
|
||||
}
|
||||
|
||||
Some(())
|
||||
})?;
|
||||
|
||||
let bounds = path.control_box().expand();
|
||||
let width = bounds.width() as u16;
|
||||
let height = bounds.height() as u16;
|
||||
|
||||
let mut ctx = vello_cpu::RenderContext::new(width, height);
|
||||
ctx.set_transform(kurbo::Affine::translate((-bounds.x0, -bounds.y0)));
|
||||
ctx.set_paint(color::OpaqueColor::<color::Srgb>::WHITE);
|
||||
ctx.fill_path(&path);
|
||||
let mut dest = vello_cpu::Pixmap::new(width, height);
|
||||
ctx.render_to_pixmap(&mut dest);
|
||||
let uv_rect = if width == 0 || height == 0 {
|
||||
UvRect::default()
|
||||
} else {
|
||||
let glyph_pos = {
|
||||
let alpha_from_coverage = atlas.options().alpha_from_coverage;
|
||||
let (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
|
||||
let pixels = dest.data_as_u8_slice();
|
||||
for y in 0..height as usize {
|
||||
for x in 0..width as usize {
|
||||
image[(x + glyph_pos.0, y + glyph_pos.1)] = alpha_from_coverage
|
||||
.color_from_coverage(
|
||||
pixels[((y * width as usize) + x) * 4 + 3] as f32 / 255.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
glyph_pos
|
||||
};
|
||||
let offset_in_pixels = vec2(bounds.x0 as f32, bounds.y0 as f32);
|
||||
let offset =
|
||||
offset_in_pixels / metrics.pixels_per_point + metrics.y_offset_in_points * Vec2::Y;
|
||||
UvRect {
|
||||
offset,
|
||||
size: vec2(width as f32, height as f32) / metrics.pixels_per_point,
|
||||
min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
|
||||
max: [
|
||||
(glyph_pos.0 + width as usize) as u16,
|
||||
(glyph_pos.1 + height as usize) as u16,
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
Some(GlyphAllocation {
|
||||
id: glyph_id,
|
||||
advance_width_px: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
|
||||
uv_rect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct VelloPen<'a> {
|
||||
path: &'a mut kurbo::BezPath,
|
||||
x_offset: f64,
|
||||
}
|
||||
|
||||
impl skrifa::outline::OutlinePen for VelloPen<'_> {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.path.move_to((x as f64 + self.x_offset, -y as f64));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.path.line_to((x as f64 + self.x_offset, -y as f64));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
|
||||
self.path.quad_to(
|
||||
(cx0 as f64 + self.x_offset, -cy0 as f64),
|
||||
(x as f64 + self.x_offset, -y as f64),
|
||||
);
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
|
||||
self.path.curve_to(
|
||||
(cx0 as f64 + self.x_offset, -cy0 as f64),
|
||||
(cx1 as f64 + self.x_offset, -cy1 as f64),
|
||||
(x as f64 + self.x_offset, -y as f64),
|
||||
);
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.path.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific font face.
|
||||
/// The interface uses points as the unit for everything.
|
||||
pub struct FontImpl {
|
||||
pub struct FontFace {
|
||||
name: String,
|
||||
ab_glyph_font: ab_glyph::FontArc,
|
||||
font: FontCell,
|
||||
tweak: FontTweak,
|
||||
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
|
||||
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
|
||||
}
|
||||
|
||||
trait FontExt {
|
||||
fn px_scale_factor(&self, scale: f32) -> f32;
|
||||
}
|
||||
impl FontFace {
|
||||
pub fn new(
|
||||
options: TextOptions,
|
||||
name: String,
|
||||
font_data: Blob,
|
||||
index: u32,
|
||||
tweak: FontTweak,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let font = FontCell::try_new(font_data, |font_data| {
|
||||
let skrifa_font =
|
||||
skrifa::FontRef::from_index(AsRef::<[u8]>::as_ref(font_data.as_ref()), index)?;
|
||||
|
||||
impl<T> FontExt for T
|
||||
where
|
||||
T: ab_glyph::Font,
|
||||
{
|
||||
fn px_scale_factor(&self, scale: f32) -> f32 {
|
||||
let units_per_em = self.units_per_em().unwrap_or_else(|| {
|
||||
panic!("The font unit size exceeds the expected range (16..=16384)")
|
||||
});
|
||||
scale / units_per_em
|
||||
}
|
||||
}
|
||||
let charmap = skrifa_font.charmap();
|
||||
let glyphs = skrifa_font.outline_glyphs();
|
||||
let metrics = skrifa_font.metrics(
|
||||
skrifa::instance::Size::unscaled(),
|
||||
skrifa::instance::LocationRef::default(),
|
||||
);
|
||||
let glyph_metrics = skrifa_font.glyph_metrics(
|
||||
skrifa::instance::Size::unscaled(),
|
||||
skrifa::instance::LocationRef::default(),
|
||||
);
|
||||
|
||||
impl FontImpl {
|
||||
pub fn new(name: String, ab_glyph_font: ab_glyph::FontArc, tweak: FontTweak) -> Self {
|
||||
Self {
|
||||
let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting);
|
||||
let hinting_instance = hinting_enabled
|
||||
.then(|| {
|
||||
// It doesn't really matter what we put here for options. Since the size is `unscaled()`, we will
|
||||
// always reconfigure this hinting instance with the real options when rendering for the first time.
|
||||
skrifa::outline::HintingInstance::new(
|
||||
&glyphs,
|
||||
skrifa::instance::Size::unscaled(),
|
||||
skrifa::instance::LocationRef::default(),
|
||||
skrifa::outline::Target::default(),
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.flatten();
|
||||
|
||||
Ok::<DependentFontData<'_>, Box<dyn std::error::Error>>(DependentFontData {
|
||||
skrifa: skrifa_font,
|
||||
charmap,
|
||||
outline_glyphs: glyphs,
|
||||
metrics,
|
||||
glyph_metrics,
|
||||
hinting_instance,
|
||||
})
|
||||
})?;
|
||||
Ok(Self {
|
||||
name,
|
||||
ab_glyph_font,
|
||||
font,
|
||||
tweak,
|
||||
glyph_info_cache: Default::default(),
|
||||
glyph_alloc_cache: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Code points that will always be replaced by the replacement character.
|
||||
@@ -223,10 +418,11 @@ impl FontImpl {
|
||||
|
||||
/// An un-ordered iterator over all supported characters.
|
||||
fn characters(&self) -> impl Iterator<Item = char> + '_ {
|
||||
self.ab_glyph_font
|
||||
.codepoint_ids()
|
||||
.map(|(_, chr)| chr)
|
||||
.filter(|&chr| !self.ignore_character(chr))
|
||||
self.font
|
||||
.borrow_dependent()
|
||||
.charmap
|
||||
.mappings()
|
||||
.filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
|
||||
}
|
||||
|
||||
/// `\n` will result in `None`
|
||||
@@ -258,7 +454,7 @@ impl FontImpl {
|
||||
// https://en.wikipedia.org/wiki/Thin_space
|
||||
|
||||
if let Some(space) = self.glyph_info(' ') {
|
||||
let em = self.ab_glyph_font.units_per_em().unwrap_or(1.0);
|
||||
let em = self.font.borrow_dependent().metrics.units_per_em as f32;
|
||||
let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5); // TODO(emilk): make configurable
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width_unscaled: advance_width.into(),
|
||||
@@ -275,52 +471,68 @@ impl FontImpl {
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
// Add new character:
|
||||
let glyph_id = self.ab_glyph_font.glyph_id(c);
|
||||
let font_data = self.font.borrow_dependent();
|
||||
|
||||
if glyph_id.0 == 0 {
|
||||
None // unsupported character
|
||||
} else {
|
||||
let glyph_info = GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
advance_width_unscaled: self.ab_glyph_font.h_advance_unscaled(glyph_id).into(),
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
Some(glyph_info)
|
||||
}
|
||||
// Add new character:
|
||||
let glyph_id = font_data
|
||||
.charmap
|
||||
.map(c)
|
||||
.filter(|id| *id != skrifa::GlyphId::NOTDEF)?;
|
||||
|
||||
let glyph_info = GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
advance_width_unscaled: font_data
|
||||
.glyph_metrics
|
||||
.advance_width(glyph_id)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
Some(glyph_info)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn pair_kerning_pixels(
|
||||
&self,
|
||||
metrics: &ScaledMetrics,
|
||||
last_glyph_id: ab_glyph::GlyphId,
|
||||
glyph_id: ab_glyph::GlyphId,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
self.ab_glyph_font.kern_unscaled(last_glyph_id, glyph_id) * metrics.px_scale_factor
|
||||
let skrifa_font = &self.font.borrow_dependent().skrifa;
|
||||
let Ok(kern) = skrifa_font.kern() else {
|
||||
return 0.0;
|
||||
};
|
||||
kern.subtables()
|
||||
.find_map(|st| match st.ok()?.kind().ok()? {
|
||||
SubtableKind::Format0(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
|
||||
SubtableKind::Format1(_) => None,
|
||||
SubtableKind::Format2(subtable2) => subtable2.kerning(last_glyph_id, glyph_id),
|
||||
SubtableKind::Format3(table_ref) => table_ref.kerning(last_glyph_id, glyph_id),
|
||||
})
|
||||
.unwrap_or_default() as f32
|
||||
* metrics.px_scale_factor
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pair_kerning(
|
||||
&self,
|
||||
metrics: &ScaledMetrics,
|
||||
last_glyph_id: ab_glyph::GlyphId,
|
||||
glyph_id: ab_glyph::GlyphId,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
|
||||
let pt_scale_factor = self
|
||||
.ab_glyph_font
|
||||
.px_scale_factor(font_size * self.tweak.scale);
|
||||
let ascent = (self.ab_glyph_font.ascent_unscaled() * pt_scale_factor).round_ui();
|
||||
let descent = (self.ab_glyph_font.descent_unscaled() * pt_scale_factor).round_ui();
|
||||
let line_gap = (self.ab_glyph_font.line_gap_unscaled() * pt_scale_factor).round_ui();
|
||||
let pt_scale_factor = self.font.px_scale_factor(font_size * self.tweak.scale);
|
||||
let font_data = self.font.borrow_dependent();
|
||||
let ascent = (font_data.metrics.ascent * pt_scale_factor).round_ui();
|
||||
let descent = (font_data.metrics.descent * pt_scale_factor).round_ui();
|
||||
let line_gap = (font_data.metrics.leading * pt_scale_factor).round_ui();
|
||||
|
||||
let scale = font_size * self.tweak.scale * pixels_per_point;
|
||||
let px_scale_factor = self.ab_glyph_font.px_scale_factor(scale);
|
||||
let px_scale_factor = self.font.px_scale_factor(scale);
|
||||
|
||||
let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
|
||||
+ self.tweak.y_offset)
|
||||
@@ -329,6 +541,7 @@ impl FontImpl {
|
||||
ScaledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
scale,
|
||||
y_offset_in_points,
|
||||
ascent,
|
||||
row_height: ascent - descent + line_gap,
|
||||
@@ -370,77 +583,20 @@ impl FontImpl {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => entry,
|
||||
};
|
||||
|
||||
debug_assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0");
|
||||
let allocation = self
|
||||
.font
|
||||
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin)
|
||||
.unwrap_or_default();
|
||||
|
||||
let uv_rect = self.ab_glyph_font.outline(glyph_id).map(|outline| {
|
||||
let glyph = ab_glyph::Glyph {
|
||||
id: glyph_id,
|
||||
// We bypass ab-glyph's scaling method because it uses the wrong scale
|
||||
// (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 {
|
||||
x: bin.as_float(),
|
||||
y: 0.0,
|
||||
},
|
||||
};
|
||||
let outlined = OutlinedGlyph::new(
|
||||
glyph,
|
||||
outline,
|
||||
ab_glyph::PxScaleFactor {
|
||||
horizontal: metrics.px_scale_factor,
|
||||
vertical: metrics.px_scale_factor,
|
||||
},
|
||||
);
|
||||
let bb = outlined.px_bounds();
|
||||
let glyph_width = bb.width() as usize;
|
||||
let glyph_height = bb.height() as usize;
|
||||
if glyph_width == 0 || glyph_height == 0 {
|
||||
UvRect::default()
|
||||
} else {
|
||||
let glyph_pos = {
|
||||
let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
|
||||
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
|
||||
outlined.draw(|x, y, v| {
|
||||
if 0.0 < v {
|
||||
let px = glyph_pos.0 + x as usize;
|
||||
let py = glyph_pos.1 + y as usize;
|
||||
image[(px, py)] = text_alpha_from_coverage.color_from_coverage(v);
|
||||
}
|
||||
});
|
||||
glyph_pos
|
||||
};
|
||||
|
||||
let offset_in_pixels = vec2(bb.min.x, bb.min.y);
|
||||
let offset = offset_in_pixels / metrics.pixels_per_point
|
||||
+ metrics.y_offset_in_points * Vec2::Y;
|
||||
UvRect {
|
||||
offset,
|
||||
size: vec2(glyph_width as f32, glyph_height as f32) / metrics.pixels_per_point,
|
||||
min: [glyph_pos.0 as u16, glyph_pos.1 as u16],
|
||||
max: [
|
||||
(glyph_pos.0 + glyph_width) as u16,
|
||||
(glyph_pos.1 + glyph_height) as u16,
|
||||
],
|
||||
}
|
||||
}
|
||||
});
|
||||
let uv_rect = uv_rect.unwrap_or_default();
|
||||
|
||||
let allocation = GlyphAllocation {
|
||||
id: glyph_id,
|
||||
advance_width_px,
|
||||
uv_rect,
|
||||
};
|
||||
entry.insert(allocation);
|
||||
(allocation, h_pos_round)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(emilk): rename?
|
||||
/// Wrapper over multiple [`FontImpl`] (e.g. a primary + fallbacks for emojis)
|
||||
/// Wrapper over multiple [`FontFace`] (e.g. a primary + fallbacks for emojis)
|
||||
pub struct Font<'a> {
|
||||
pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
|
||||
pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
pub(super) cached_family: &'a mut CachedFamily,
|
||||
pub(super) atlas: &'a mut TextureAtlas,
|
||||
}
|
||||
@@ -471,7 +627,7 @@ impl Font<'_> {
|
||||
.fonts
|
||||
.first()
|
||||
.and_then(|key| self.fonts_by_id.get(key))
|
||||
.map(|font_impl| font_impl.scaled_metrics(pixels_per_point, font_size))
|
||||
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -479,7 +635,7 @@ impl Font<'_> {
|
||||
pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
|
||||
let (key, glyph_info) = self.glyph_info(c);
|
||||
if let Some(font) = &self.fonts_by_id.get(&key) {
|
||||
glyph_info.advance_width_unscaled.0 * font.ab_glyph_font.px_scale_factor(font_size)
|
||||
glyph_info.advance_width_unscaled.0 * font.font.px_scale_factor(font_size)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
@@ -524,7 +680,10 @@ pub struct ScaledMetrics {
|
||||
/// Translates "unscaled" units to physical (screen) pixels.
|
||||
pub px_scale_factor: f32,
|
||||
|
||||
/// Vertical offset, in UI points.
|
||||
/// Absolute scale in screen pixels, for skrifa.
|
||||
pub scale: f32,
|
||||
|
||||
/// Vertical offset, in UI points (not screen-space).
|
||||
pub y_offset_in_points: f32,
|
||||
|
||||
/// This is the distance from the top to the baseline.
|
||||
@@ -540,7 +699,7 @@ pub struct ScaledMetrics {
|
||||
|
||||
/// Code points that will always be invisible (zero width).
|
||||
///
|
||||
/// See also [`FontImpl::ignore_character`].
|
||||
/// See also [`FontFace::ignore_character`].
|
||||
#[inline]
|
||||
fn invisible_char(c: char) -> bool {
|
||||
if c == '\r' {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -7,10 +8,10 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AlphaFromCoverage, TextureAtlas,
|
||||
TextureAtlas,
|
||||
text::{
|
||||
Galley, LayoutJob, LayoutSection,
|
||||
font::{Font, FontImpl, GlyphInfo},
|
||||
Galley, LayoutJob, LayoutSection, TextOptions,
|
||||
font::{Font, FontFace, GlyphInfo},
|
||||
},
|
||||
};
|
||||
use emath::{NumExt as _, OrderedFloat};
|
||||
@@ -116,7 +117,7 @@ impl std::fmt::Display for FontFamily {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FontData {
|
||||
/// The content of a `.ttf` or `.otf` file.
|
||||
pub font: std::borrow::Cow<'static, [u8]>,
|
||||
pub font: Cow<'static, [u8]>,
|
||||
|
||||
/// Which font face in the file to use.
|
||||
/// When in doubt, use `0`.
|
||||
@@ -129,7 +130,7 @@ pub struct FontData {
|
||||
impl FontData {
|
||||
pub fn from_static(font: &'static [u8]) -> Self {
|
||||
Self {
|
||||
font: std::borrow::Cow::Borrowed(font),
|
||||
font: Cow::Borrowed(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
}
|
||||
@@ -137,7 +138,7 @@ impl FontData {
|
||||
|
||||
pub fn from_owned(font: Vec<u8>) -> Self {
|
||||
Self {
|
||||
font: std::borrow::Cow::Owned(font),
|
||||
font: Cow::Owned(font),
|
||||
index: 0,
|
||||
tweak: Default::default(),
|
||||
}
|
||||
@@ -184,6 +185,11 @@ pub struct FontTweak {
|
||||
///
|
||||
/// Example value: `2.0`.
|
||||
pub y_offset: f32,
|
||||
|
||||
/// Override the global font hinting setting for this specific font.
|
||||
///
|
||||
/// `None` means use the global setting.
|
||||
pub hinting_override: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for FontTweak {
|
||||
@@ -192,24 +198,20 @@ impl Default for FontTweak {
|
||||
scale: 1.0,
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
hinting_override: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontArc {
|
||||
match &data.font {
|
||||
std::borrow::Cow::Borrowed(bytes) => {
|
||||
ab_glyph::FontRef::try_from_slice_and_index(bytes, data.index)
|
||||
.map(ab_glyph::FontArc::from)
|
||||
}
|
||||
std::borrow::Cow::Owned(bytes) => {
|
||||
ab_glyph::FontVec::try_from_vec_and_index(bytes.clone(), data.index)
|
||||
.map(ab_glyph::FontArc::from)
|
||||
}
|
||||
pub type Blob = Arc<dyn AsRef<[u8]> + Send + Sync>;
|
||||
|
||||
fn blob_from_font_data(data: &FontData) -> Blob {
|
||||
match data.clone().font {
|
||||
Cow::Borrowed(bytes) => Arc::new(bytes) as Blob,
|
||||
Cow::Owned(bytes) => Arc::new(bytes) as Blob,
|
||||
}
|
||||
.unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"))
|
||||
}
|
||||
|
||||
/// Describes the font data and the sizes to use.
|
||||
@@ -438,7 +440,7 @@ pub(super) struct CachedFamily {
|
||||
impl CachedFamily {
|
||||
fn new(
|
||||
fonts: Vec<FontFaceKey>,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Self {
|
||||
if fonts.is_empty() {
|
||||
return Self {
|
||||
@@ -476,11 +478,11 @@ impl CachedFamily {
|
||||
pub(crate) fn glyph_info_no_cache_or_fallback(
|
||||
&mut self,
|
||||
c: char,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Option<(FontFaceKey, GlyphInfo)> {
|
||||
for font_key in &self.fonts {
|
||||
let font_impl = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID");
|
||||
if let Some(glyph_info) = font_impl.glyph_info(c) {
|
||||
let font_face = fonts_by_id.get_mut(font_key).expect("Nonexistent font ID");
|
||||
if let Some(glyph_info) = font_face.glyph_info(c) {
|
||||
self.glyph_info_cache.insert(c, (*font_key, glyph_info));
|
||||
return Some((*font_key, glyph_info));
|
||||
}
|
||||
@@ -508,43 +510,29 @@ pub struct Fonts {
|
||||
impl Fonts {
|
||||
/// Create a new [`Fonts`] for text layout.
|
||||
/// This call is expensive, so only create one [`Fonts`] and then reuse it.
|
||||
///
|
||||
/// * `max_texture_side`: largest supported texture size (one side).
|
||||
pub fn new(
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
|
||||
Self {
|
||||
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
|
||||
fonts: FontsImpl::new(options, definitions),
|
||||
galley_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Call at the start of each frame with the latest known
|
||||
/// `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`.
|
||||
/// Call at the start of each frame with the latest known [`TextOptions`].
|
||||
///
|
||||
/// Call after painting the previous frame, but before using [`Fonts`] for the new frame.
|
||||
///
|
||||
/// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`,
|
||||
/// This function will react to changes in [`TextOptions`],
|
||||
/// as well as notice when the font atlas is getting full, and handle that.
|
||||
pub fn begin_pass(
|
||||
&mut self,
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
) {
|
||||
let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side;
|
||||
let text_alpha_from_coverage_changed =
|
||||
self.fonts.atlas.text_alpha_from_coverage != text_alpha_from_coverage;
|
||||
pub fn begin_pass(&mut self, options: TextOptions) {
|
||||
let text_options_changed = self.fonts.options() != &options;
|
||||
let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8;
|
||||
let needs_recreate =
|
||||
max_texture_side_changed || text_alpha_from_coverage_changed || font_atlas_almost_full;
|
||||
let needs_recreate = text_options_changed || font_atlas_almost_full;
|
||||
|
||||
if needs_recreate {
|
||||
let definitions = self.fonts.definitions.clone();
|
||||
|
||||
*self = Self {
|
||||
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
|
||||
fonts: FontsImpl::new(options, definitions),
|
||||
galley_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
@@ -558,8 +546,8 @@ impl Fonts {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn max_texture_side(&self) -> usize {
|
||||
self.fonts.max_texture_side
|
||||
pub fn options(&self) -> &TextOptions {
|
||||
self.texture_atlas().options()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -628,8 +616,8 @@ pub struct FontsView<'a> {
|
||||
|
||||
impl FontsView<'_> {
|
||||
#[inline]
|
||||
pub fn max_texture_side(&self) -> usize {
|
||||
self.fonts.max_texture_side
|
||||
pub fn options(&self) -> &TextOptions {
|
||||
self.fonts.options()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -671,6 +659,7 @@ impl FontsView<'_> {
|
||||
/// Height of one row of text in points.
|
||||
///
|
||||
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
|
||||
#[inline]
|
||||
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
|
||||
self.fonts
|
||||
.font(&font_id.family)
|
||||
@@ -716,6 +705,7 @@ impl FontsView<'_> {
|
||||
/// Will wrap text at the given width and line break at `\n`.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
#[inline]
|
||||
pub fn layout(
|
||||
&mut self,
|
||||
text: String,
|
||||
@@ -730,6 +720,7 @@ impl FontsView<'_> {
|
||||
/// Will line break at `\n`.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
#[inline]
|
||||
pub fn layout_no_wrap(
|
||||
&mut self,
|
||||
text: String,
|
||||
@@ -743,6 +734,7 @@ impl FontsView<'_> {
|
||||
/// Like [`Self::layout`], made for when you want to pick a color for the text later.
|
||||
///
|
||||
/// The implementation uses memoization so repeated calls are cheap.
|
||||
#[inline]
|
||||
pub fn layout_delayed_color(
|
||||
&mut self,
|
||||
text: String,
|
||||
@@ -759,10 +751,9 @@ impl FontsView<'_> {
|
||||
///
|
||||
/// Required in order to paint text.
|
||||
pub struct FontsImpl {
|
||||
max_texture_side: usize,
|
||||
definitions: FontDefinitions,
|
||||
atlas: TextureAtlas,
|
||||
fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl>,
|
||||
fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
fonts_by_name: ahash::HashMap<String, FontFaceKey>,
|
||||
family_cache: ahash::HashMap<FontFamily, CachedFamily>,
|
||||
}
|
||||
@@ -770,36 +761,36 @@ pub struct FontsImpl {
|
||||
impl FontsImpl {
|
||||
/// Create a new [`FontsImpl`] for text layout.
|
||||
/// This call is expensive, so only create one [`FontsImpl`] and then reuse it.
|
||||
pub fn new(
|
||||
max_texture_side: usize,
|
||||
text_alpha_from_coverage: AlphaFromCoverage,
|
||||
definitions: FontDefinitions,
|
||||
) -> Self {
|
||||
let texture_width = max_texture_side.at_most(16 * 1024);
|
||||
pub fn new(options: TextOptions, definitions: FontDefinitions) -> Self {
|
||||
let texture_width = options.max_texture_side.at_most(16 * 1024);
|
||||
let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways.
|
||||
let atlas = TextureAtlas::new([texture_width, initial_height], text_alpha_from_coverage);
|
||||
let atlas = TextureAtlas::new([texture_width, initial_height], options);
|
||||
|
||||
let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl> = Default::default();
|
||||
let mut font_impls: ahash::HashMap<String, FontFaceKey> = Default::default();
|
||||
let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace> = Default::default();
|
||||
let mut fonts_by_name: ahash::HashMap<String, FontFaceKey> = Default::default();
|
||||
for (name, font_data) in &definitions.font_data {
|
||||
let tweak = font_data.tweak;
|
||||
let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
|
||||
let font_impl = FontImpl::new(name.clone(), ab_glyph, tweak);
|
||||
let blob = blob_from_font_data(font_data);
|
||||
let font_face = FontFace::new(options, name.clone(), blob, font_data.index, tweak)
|
||||
.unwrap_or_else(|err| panic!("Error parsing {name:?} TTF/OTF font file: {err}"));
|
||||
let key = FontFaceKey::new();
|
||||
fonts_by_id.insert(key, font_impl);
|
||||
font_impls.insert(name.clone(), key);
|
||||
fonts_by_id.insert(key, font_face);
|
||||
fonts_by_name.insert(name.clone(), key);
|
||||
}
|
||||
|
||||
Self {
|
||||
max_texture_side,
|
||||
definitions,
|
||||
atlas,
|
||||
fonts_by_id,
|
||||
fonts_by_name: font_impls,
|
||||
fonts_by_name,
|
||||
family_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &TextOptions {
|
||||
self.atlas.options()
|
||||
}
|
||||
|
||||
/// Get the right font implementation from [`FontFamily`].
|
||||
pub fn font(&mut self, family: &FontFamily) -> Font<'_> {
|
||||
let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| {
|
||||
@@ -1192,12 +1183,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_split_paragraphs() {
|
||||
for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
|
||||
let max_texture_side = 4096;
|
||||
let mut fonts = FontsImpl::new(
|
||||
max_texture_side,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
for halign in [Align::Min, Align::Center, Align::Max] {
|
||||
for justify in [false, true] {
|
||||
@@ -1255,11 +1241,7 @@ mod tests {
|
||||
let rounded_output_to_gui = [false, true];
|
||||
|
||||
for pixels_per_point in pixels_per_point {
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
for &max_width in &max_widths {
|
||||
for round_output_to_gui in rounded_output_to_gui {
|
||||
@@ -1306,7 +1288,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_fallback_glyph_width() {
|
||||
let mut fonts = Fonts::new(1024, AlphaFromCoverage::default(), FontDefinitions::empty());
|
||||
let mut fonts = Fonts::new(TextOptions::default(), FontDefinitions::empty());
|
||||
let mut view = fonts.with_pixels_per_point(1.0);
|
||||
|
||||
let width = view.glyph_width(&FontId::new(12.0, FontFamily::Proportional), ' ');
|
||||
|
||||
@@ -20,3 +20,31 @@ pub use {
|
||||
|
||||
/// Suggested character to use to replace those in password text fields.
|
||||
pub const PASSWORD_REPLACEMENT_CHAR: char = '•';
|
||||
|
||||
/// Controls how we render text
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TextOptions {
|
||||
/// Maximum size of the font texture.
|
||||
pub max_texture_side: usize,
|
||||
|
||||
/// Controls how to convert glyph coverage to alpha.
|
||||
pub alpha_from_coverage: crate::AlphaFromCoverage,
|
||||
|
||||
/// Whether to enable font hinting
|
||||
///
|
||||
/// (round some font coordinates to pixels for sharper text).
|
||||
///
|
||||
/// Default is `true`.
|
||||
pub font_hinting: bool,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_texture_side: 2048, // Small but portable
|
||||
alpha_from_coverage: crate::AlphaFromCoverage::default(),
|
||||
font_hinting: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ fn layout_section(
|
||||
|
||||
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
|
||||
let mut current_font = FontFaceKey::INVALID;
|
||||
let mut current_font_impl_metrics = ScaledMetrics::default();
|
||||
let mut current_font_face_metrics = ScaledMetrics::default();
|
||||
|
||||
for chr in job.text[byte_range.clone()].chars() {
|
||||
if job.break_on_newline && chr == '\n' {
|
||||
@@ -185,20 +185,20 @@ fn layout_section(
|
||||
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
|
||||
} else {
|
||||
let (font_id, glyph_info) = font.glyph_info(chr);
|
||||
let mut font_impl = font.fonts_by_id.get_mut(&font_id);
|
||||
let mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
if current_font != font_id {
|
||||
current_font = font_id;
|
||||
current_font_impl_metrics = font_impl
|
||||
current_font_face_metrics = font_face
|
||||
.as_ref()
|
||||
.map(|font_impl| font_impl.scaled_metrics(pixels_per_point, font_size))
|
||||
.map(|font_face| font_face.scaled_metrics(pixels_per_point, font_size))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
if let (Some(font_impl), Some(last_glyph_id), Some(glyph_id)) =
|
||||
(&font_impl, last_glyph_id, glyph_info.id)
|
||||
if let (Some(font_face), Some(last_glyph_id), Some(glyph_id)) =
|
||||
(&font_face, last_glyph_id, glyph_info.id)
|
||||
{
|
||||
paragraph.cursor_x_px += font_impl.pair_kerning_pixels(
|
||||
¤t_font_impl_metrics,
|
||||
paragraph.cursor_x_px += font_face.pair_kerning_pixels(
|
||||
¤t_font_face_metrics,
|
||||
last_glyph_id,
|
||||
glyph_id,
|
||||
);
|
||||
@@ -207,10 +207,10 @@ fn layout_section(
|
||||
paragraph.cursor_x_px += extra_letter_spacing * pixels_per_point;
|
||||
}
|
||||
|
||||
let (glyph_alloc, physical_x) = if let Some(font_impl) = font_impl.as_mut() {
|
||||
font_impl.allocate_glyph(
|
||||
let (glyph_alloc, physical_x) = if let Some(font_face) = font_face.as_mut() {
|
||||
font_face.allocate_glyph(
|
||||
font.atlas,
|
||||
¤t_font_impl_metrics,
|
||||
¤t_font_face_metrics,
|
||||
glyph_info,
|
||||
chr,
|
||||
paragraph.cursor_x_px,
|
||||
@@ -224,8 +224,8 @@ fn layout_section(
|
||||
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
|
||||
advance_width: glyph_alloc.advance_width_px / pixels_per_point,
|
||||
line_height,
|
||||
font_impl_height: current_font_impl_metrics.row_height,
|
||||
font_impl_ascent: current_font_impl_metrics.ascent,
|
||||
font_face_height: current_font_face_metrics.row_height,
|
||||
font_face_ascent: current_font_face_metrics.ascent,
|
||||
font_height: font_metrics.row_height,
|
||||
font_ascent: font_metrics.ascent,
|
||||
uv_rect: glyph_alloc.uv_rect,
|
||||
@@ -463,22 +463,22 @@ fn replace_last_glyph_with_overflow_character(
|
||||
let font_size = section.format.font_id.size;
|
||||
|
||||
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
|
||||
let mut font_face = font.fonts_by_id.get_mut(&font_id);
|
||||
let font_face_metrics = font_face
|
||||
.as_mut()
|
||||
.map(|f| f.scaled_metrics(pixels_per_point, font_size))
|
||||
.unwrap_or_default();
|
||||
|
||||
let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
|
||||
// Kern the overflow character properly
|
||||
let pair_kerning = font_impl
|
||||
let pair_kerning = font_face
|
||||
.as_mut()
|
||||
.map(|font_impl| {
|
||||
.map(|font_face| {
|
||||
if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = (
|
||||
font_impl.glyph_info(prev_glyph.chr).and_then(|g| g.id),
|
||||
font_impl.glyph_info(overflow_character).and_then(|g| g.id),
|
||||
font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id),
|
||||
font_face.glyph_info(overflow_character).and_then(|g| g.id),
|
||||
) {
|
||||
font_impl.pair_kerning(&font_impl_metrics, prev_glyph_id, overflow_glyph_id)
|
||||
font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
@@ -490,10 +490,10 @@ fn replace_last_glyph_with_overflow_character(
|
||||
0.0 // TODO(emilk): heed paragraph leading_space 😬
|
||||
};
|
||||
|
||||
let replacement_glyph_width = font_impl
|
||||
let replacement_glyph_width = font_face
|
||||
.as_mut()
|
||||
.and_then(|f| f.glyph_info(overflow_character))
|
||||
.map(|i| i.advance_width_unscaled.0 * font_impl_metrics.px_scale_factor)
|
||||
.map(|i| i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if we're within width budget:
|
||||
@@ -502,12 +502,12 @@ fn replace_last_glyph_with_overflow_character(
|
||||
{
|
||||
// we are done
|
||||
|
||||
let (replacement_glyph_alloc, physical_x) = font_impl
|
||||
let (replacement_glyph_alloc, physical_x) = font_face
|
||||
.as_mut()
|
||||
.map(|f| {
|
||||
f.allocate_glyph(
|
||||
font.atlas,
|
||||
&font_impl_metrics,
|
||||
&font_face_metrics,
|
||||
glyph_info,
|
||||
overflow_character,
|
||||
overflow_glyph_x * pixels_per_point,
|
||||
@@ -526,8 +526,8 @@ fn replace_last_glyph_with_overflow_character(
|
||||
pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
|
||||
advance_width: replacement_glyph_alloc.advance_width_px / pixels_per_point,
|
||||
line_height,
|
||||
font_impl_height: font_impl_metrics.row_height,
|
||||
font_impl_ascent: font_impl_metrics.ascent,
|
||||
font_face_height: font_face_metrics.row_height,
|
||||
font_face_ascent: font_face_metrics.ascent,
|
||||
font_height: font_metrics.row_height,
|
||||
font_ascent: font_metrics.ascent,
|
||||
uv_rect: replacement_glyph_alloc.uv_rect,
|
||||
@@ -668,14 +668,14 @@ fn galley_from_rows(
|
||||
for glyph in &mut row.glyphs {
|
||||
let format = &job.sections[glyph.section_index as usize].format;
|
||||
|
||||
glyph.pos.y = glyph.font_impl_ascent
|
||||
glyph.pos.y = glyph.font_face_ascent
|
||||
|
||||
// Apply valign to the different in height of the entire row, and the height of this `Font`:
|
||||
+ format.valign.to_factor() * (max_row_height - glyph.line_height)
|
||||
|
||||
// When mixing different `FontImpl` (e.g. latin and emojis),
|
||||
// we always center the difference:
|
||||
+ 0.5 * (glyph.font_height - glyph.font_impl_height);
|
||||
+ 0.5 * (glyph.font_height - glyph.font_face_height);
|
||||
|
||||
glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y);
|
||||
}
|
||||
@@ -1050,18 +1050,13 @@ impl RowBreakCandidates {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::AlphaFromCoverage;
|
||||
|
||||
use super::{super::*, *};
|
||||
|
||||
#[test]
|
||||
fn test_zero_max_width() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = 0.0;
|
||||
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
|
||||
@@ -1074,11 +1069,7 @@ mod tests {
|
||||
|
||||
let pixels_per_point = 1.0;
|
||||
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let text_format = TextFormat {
|
||||
font_id: FontId::monospace(12.0),
|
||||
..Default::default()
|
||||
@@ -1124,11 +1115,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_cjk() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
@@ -1144,11 +1131,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_pre_cjk() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let mut layout_job = LayoutJob::single_section(
|
||||
"日本語とEnglishの混在した文章".into(),
|
||||
TextFormat::default(),
|
||||
@@ -1164,11 +1147,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_truncate_width() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
let mut layout_job =
|
||||
LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
|
||||
layout_job.wrap.max_width = f32::INFINITY;
|
||||
@@ -1188,11 +1167,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_row() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let font_id = FontId::default();
|
||||
let font_height = fonts
|
||||
@@ -1225,11 +1200,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_end_with_newline() {
|
||||
let pixels_per_point = 1.0;
|
||||
let mut fonts = FontsImpl::new(
|
||||
1024,
|
||||
AlphaFromCoverage::default(),
|
||||
FontDefinitions::default(),
|
||||
);
|
||||
let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
|
||||
|
||||
let font_id = FontId::default();
|
||||
let font_height = fonts
|
||||
|
||||
@@ -686,11 +686,11 @@ pub struct Glyph {
|
||||
/// The row/line height of this font.
|
||||
pub font_height: f32,
|
||||
|
||||
/// The ascent of the sub-font within the font (`FontImpl`).
|
||||
pub font_impl_ascent: f32,
|
||||
/// The ascent of the sub-font within the font (`FontFace`).
|
||||
pub font_face_ascent: f32,
|
||||
|
||||
/// The row/line height of the sub-font within the font (`FontImpl`).
|
||||
pub font_impl_height: f32,
|
||||
/// The row/line height of the sub-font within the font (`FontFace`).
|
||||
pub font_face_height: f32,
|
||||
|
||||
/// Position and size of the glyph in the font texture, in texels.
|
||||
pub uv_rect: UvRect,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ecolor::Color32;
|
||||
use emath::{Rect, remap_clamp};
|
||||
|
||||
use crate::{AlphaFromCoverage, ColorImage, ImageDelta};
|
||||
use crate::{ColorImage, ImageDelta, TextOptions};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct Rectu {
|
||||
@@ -75,11 +75,11 @@ pub struct TextureAtlas {
|
||||
discs: Vec<PrerasterizedDisc>,
|
||||
|
||||
/// Controls how to convert glyph coverage to alpha.
|
||||
pub(crate) text_alpha_from_coverage: AlphaFromCoverage,
|
||||
options: TextOptions,
|
||||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
pub fn new(size: [usize; 2], text_alpha_from_coverage: AlphaFromCoverage) -> Self {
|
||||
pub fn new(size: [usize; 2], options: TextOptions) -> Self {
|
||||
assert!(size[0] >= 1024, "Tiny texture atlas");
|
||||
let mut atlas = Self {
|
||||
image: ColorImage::filled(size, Color32::TRANSPARENT),
|
||||
@@ -88,7 +88,7 @@ impl TextureAtlas {
|
||||
row_height: 0,
|
||||
overflowed: false,
|
||||
discs: vec![], // will be filled in below
|
||||
text_alpha_from_coverage,
|
||||
options,
|
||||
};
|
||||
|
||||
// Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
|
||||
@@ -121,7 +121,7 @@ impl TextureAtlas {
|
||||
let coverage =
|
||||
remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
|
||||
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
|
||||
text_alpha_from_coverage.color_from_coverage(coverage);
|
||||
options.alpha_from_coverage.color_from_coverage(coverage);
|
||||
}
|
||||
}
|
||||
atlas.discs.push(PrerasterizedDisc {
|
||||
@@ -138,6 +138,10 @@ impl TextureAtlas {
|
||||
atlas
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &TextOptions {
|
||||
&self.options
|
||||
}
|
||||
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.image.size
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user