mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 05:10:03 -04:00
Merge branch 'main' into lucas/malmal/main
Re-port the WASM canvas font fallback onto main's new skrifa/harfrust shaping pipeline: - Add `chr` to `ShapedGlyph` and trigger the canvas fallback from `allocate_glyph` on NOTDEF (WASM only). - Drive the fallback from `text_layout`'s NOTDEF branch via `has_glyph`, so it only kicks in when no loaded font contains the character. - Update `allocate_canvas_glyph` for main's `GlyphAllocation` (uv_rect only) and `color_transfer_function` API; fix `get_image_data` f64 args. Merge the per-axis scroll fade gating into main's refactored `paint_fade_areas_impl`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,14 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.34.3 - 2026-05-27
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.34.2 - 2026-05-04
|
||||
* Fix text layout bugs in wrapped texts [#8137](https://github.com/emilk/egui/pull/8137) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.34.1 - 2026-03-27
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -48,7 +48,14 @@ mint = ["emath/mint"]
|
||||
rayon = ["dep:rayon"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"]
|
||||
serde = [
|
||||
"dep:serde",
|
||||
"ahash/serde",
|
||||
"ecolor/serde",
|
||||
"emath/serde",
|
||||
"font-types/serde",
|
||||
"smallvec/serde",
|
||||
]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = []
|
||||
@@ -63,6 +70,7 @@ ecolor.workspace = true
|
||||
|
||||
ahash.workspace = true
|
||||
font-types.workspace = true
|
||||
harfrust.workspace = true
|
||||
log.workspace = true
|
||||
nohash-hasher.workspace = true
|
||||
parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios.
|
||||
@@ -70,6 +78,8 @@ profiling.workspace = true
|
||||
self_cell.workspace = true
|
||||
skrifa.workspace = true
|
||||
smallvec.workspace = true
|
||||
unicode-general-category.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
vello_cpu.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ecolor::linear_f32_from_linear_u8;
|
||||
use emath::Vec2;
|
||||
|
||||
use crate::{Color32, textures::TextureOptions};
|
||||
@@ -346,22 +347,37 @@ impl std::fmt::Debug for ColorImage {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// How to convert font coverage values into alpha and color values.
|
||||
//
|
||||
// This whole thing is less than rigorous.
|
||||
// Ideally we should do this in a shader instead, and use different computations
|
||||
// for different text colors.
|
||||
// See https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html for an in-depth analysis.
|
||||
///
|
||||
/// epaint stores all glyphs in the font atlas as white (with varying opacity),
|
||||
/// so that egui can reuse the same glyph for different text colors
|
||||
/// (with a simple color multiplication in the shader).
|
||||
///
|
||||
/// Because of this simplification, we need to apply a non-linear
|
||||
/// ramp to the glyph colors before writing them into the font atlas,
|
||||
/// as a way to compensate.
|
||||
///
|
||||
/// This whole thing is less than rigorous.
|
||||
///
|
||||
/// It would be better to either render all text colors into the font atlas
|
||||
/// (which would require more atlas space, but would allow for more accurate rendering of colored text and emojis),
|
||||
/// or do the color compensation in the shader, based on the active text color.
|
||||
///
|
||||
/// When experimenting, use <https://fonts.google.com/specimen/Ubuntu> to compare to a ground truth.
|
||||
///
|
||||
/// See <https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html> for related analysis.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum AlphaFromCoverage {
|
||||
/// `alpha = coverage`.
|
||||
pub enum FontColorTransferFunction {
|
||||
/// Use the raw RGBA values from the font rasterizer, without any conversion.
|
||||
///
|
||||
/// Looks good for black-on-white text, i.e. light mode.
|
||||
/// This is the required mode for colored emojis etc.
|
||||
///
|
||||
/// Same as [`Self::Gamma`]`(1.0)`, but more efficient.
|
||||
Linear,
|
||||
/// This mode looks good for black-on-white text, i.e. light mode.
|
||||
Off,
|
||||
|
||||
/// `alpha = coverage^gamma`.
|
||||
///
|
||||
/// Gamma=1 looks good for black-on-white text, i.e. light mode.
|
||||
Gamma(f32),
|
||||
|
||||
/// `alpha = 2 * coverage - coverage^2`
|
||||
@@ -374,29 +390,59 @@ pub enum AlphaFromCoverage {
|
||||
TwoCoverageMinusCoverageSq,
|
||||
}
|
||||
|
||||
impl AlphaFromCoverage {
|
||||
impl FontColorTransferFunction {
|
||||
/// A good-looking default for light mode (black-on-white text).
|
||||
pub const LIGHT_MODE_DEFAULT: Self = Self::Linear;
|
||||
pub const LIGHT_MODE_DEFAULT: Self = Self::Off;
|
||||
|
||||
/// A good-looking default for dark mode (white-on-black text).
|
||||
pub const DARK_MODE_DEFAULT: Self = Self::TwoCoverageMinusCoverageSq;
|
||||
|
||||
/// How to convert a white color written by the font rasterizer
|
||||
/// into a color to be written into the font atlas.
|
||||
#[inline(always)]
|
||||
pub fn to_atlas_color(self, input_color: Color32) -> Color32 {
|
||||
match self {
|
||||
Self::Off | Self::Gamma(1.0) => input_color,
|
||||
|
||||
Self::Gamma(gamma) => {
|
||||
let coverage = linear_f32_from_linear_u8(input_color.a());
|
||||
let alpha = coverage.powf(gamma);
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
}
|
||||
|
||||
Self::TwoCoverageMinusCoverageSq => {
|
||||
let coverage = linear_f32_from_linear_u8(input_color.a());
|
||||
let alpha = 2.0 * coverage - coverage * coverage;
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert coverage to alpha.
|
||||
#[inline(always)]
|
||||
pub fn alpha_from_coverage(&self, coverage: f32) -> f32 {
|
||||
pub fn alpha_from_coverage(self, coverage: f32) -> f32 {
|
||||
let coverage = coverage.clamp(0.0, 1.0);
|
||||
match self {
|
||||
Self::Linear => coverage,
|
||||
Self::Gamma(gamma) => coverage.powf(*gamma),
|
||||
Self::Off | Self::Gamma(1.0) => coverage,
|
||||
Self::Gamma(gamma) => coverage.powf(gamma),
|
||||
Self::TwoCoverageMinusCoverageSq => 2.0 * coverage - coverage * coverage,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn color_from_coverage(&self, coverage: f32) -> Color32 {
|
||||
pub fn color_from_coverage(self, coverage: f32) -> Color32 {
|
||||
let alpha = self.alpha_from_coverage(coverage);
|
||||
Color32::from_white_alpha(ecolor::linear_u8_from_linear_f32(alpha))
|
||||
}
|
||||
|
||||
/// Convert this into the closest gamma exponent
|
||||
pub fn to_gamma(self) -> f32 {
|
||||
match self {
|
||||
Self::Off => 1.0,
|
||||
Self::Gamma(gamma) => gamma,
|
||||
Self::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -52,7 +52,7 @@ pub use self::{
|
||||
corner_radius::CornerRadius,
|
||||
corner_radius_f32::CornerRadiusF32,
|
||||
direction::Direction,
|
||||
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
|
||||
image::{ColorImage, FontColorTransferFunction, ImageData, ImageDelta},
|
||||
margin::Margin,
|
||||
margin_f32::*,
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
@@ -71,15 +71,9 @@ pub use self::{
|
||||
viewport::ViewportInPixels,
|
||||
};
|
||||
|
||||
#[deprecated = "Renamed to CornerRadius"]
|
||||
pub type Rounding = CornerRadius;
|
||||
|
||||
pub use ecolor::{Color32, Hsva, HsvaGamma, Rgba};
|
||||
pub use emath::{Pos2, Rect, Vec2, pos2, vec2};
|
||||
|
||||
#[deprecated = "Use the ahash crate directly."]
|
||||
pub use ahash;
|
||||
|
||||
pub use ecolor;
|
||||
pub use emath;
|
||||
|
||||
|
||||
@@ -17,9 +17,6 @@ pub struct MarginF32 {
|
||||
pub bottom: f32,
|
||||
}
|
||||
|
||||
#[deprecated = "Renamed to MarginF32"]
|
||||
pub type Marginf = MarginF32;
|
||||
|
||||
impl From<Margin> for MarginF32 {
|
||||
#[inline]
|
||||
fn from(margin: Margin) -> Self {
|
||||
@@ -97,18 +94,6 @@ impl MarginF32 {
|
||||
pub fn is_same(&self) -> bool {
|
||||
self.left == self.right && self.left == self.top && self.left == self.bottom
|
||||
}
|
||||
|
||||
#[deprecated = "Use `rect + margin` instead"]
|
||||
#[inline]
|
||||
pub fn expand_rect(&self, rect: Rect) -> Rect {
|
||||
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
|
||||
}
|
||||
|
||||
#[deprecated = "Use `rect - margin` instead"]
|
||||
#[inline]
|
||||
pub fn shrink_rect(&self, rect: Rect) -> Rect {
|
||||
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for MarginF32 {
|
||||
|
||||
@@ -357,12 +357,6 @@ impl Shape {
|
||||
.into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[deprecated = "Use `Shape::galley` or `Shape::galley_with_override_text_color` instead"]
|
||||
pub fn galley_with_color(pos: Pos2, galley: Arc<Galley>, text_color: Color32) -> Self {
|
||||
Self::galley_with_override_text_color(pos, galley, text_color)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mesh(mesh: impl Into<Arc<Mesh>>) -> Self {
|
||||
let mesh = mesh.into();
|
||||
|
||||
@@ -135,10 +135,10 @@ impl AllocInfo {
|
||||
what,
|
||||
self.megabytes()
|
||||
)
|
||||
} else if self.element_size != ElementSize::Heterogenous {
|
||||
} else if self.element_size == ElementSize::Heterogenous {
|
||||
format!(
|
||||
"{:6} {:16} {} {:3} allocations",
|
||||
self.num_elements(),
|
||||
"",
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
@@ -146,7 +146,7 @@ impl AllocInfo {
|
||||
} else {
|
||||
format!(
|
||||
"{:6} {:16} {} {:3} allocations",
|
||||
"",
|
||||
self.num_elements(),
|
||||
what,
|
||||
self.megabytes(),
|
||||
self.num_allocs()
|
||||
|
||||
@@ -22,9 +22,9 @@ impl Stroke {
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
||||
pub fn new(width: f32, color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
width,
|
||||
color: color.into(),
|
||||
}
|
||||
}
|
||||
@@ -136,9 +136,9 @@ impl PathStroke {
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn new(width: impl Into<f32>, color: impl Into<Color32>) -> Self {
|
||||
pub fn new(width: f32, color: impl Into<Color32>) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
width,
|
||||
color: ColorMode::Solid(color.into()),
|
||||
kind: StrokeKind::Middle,
|
||||
}
|
||||
@@ -149,11 +149,11 @@ impl PathStroke {
|
||||
/// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`)
|
||||
#[inline]
|
||||
pub fn new_uv(
|
||||
width: impl Into<f32>,
|
||||
width: f32,
|
||||
callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
width: width.into(),
|
||||
width,
|
||||
color: ColorMode::UV(Arc::new(callback)),
|
||||
kind: StrokeKind::Middle,
|
||||
}
|
||||
|
||||
@@ -1703,16 +1703,6 @@ impl Tessellator {
|
||||
.stroke_open(self.feathering, &stroke.into(), out);
|
||||
}
|
||||
|
||||
#[deprecated = "Use `tessellate_line_segment` instead"]
|
||||
pub fn tessellate_line(
|
||||
&mut self,
|
||||
points: [Pos2; 2],
|
||||
stroke: impl Into<Stroke>,
|
||||
out: &mut Mesh,
|
||||
) {
|
||||
self.tessellate_line_segment(points, stroke, out);
|
||||
}
|
||||
|
||||
/// Tessellate a single [`PathShape`] into a [`Mesh`].
|
||||
///
|
||||
/// * `path_shape`: the path to tessellate.
|
||||
|
||||
@@ -15,8 +15,6 @@ pub struct CanvasGlyphData {
|
||||
pub width: u32,
|
||||
/// Height of the glyph in pixels
|
||||
pub height: u32,
|
||||
/// Advance width for horizontal text layout
|
||||
pub advance_width: f32,
|
||||
/// Horizontal offset from origin
|
||||
pub offset_x: f32,
|
||||
/// Vertical offset from origin (baseline)
|
||||
@@ -162,7 +160,7 @@ impl CanvasGlyphRenderer {
|
||||
// Extract image data (now at device pixel resolution)
|
||||
let image_data = self
|
||||
.context
|
||||
.get_image_data(0, 0, width as i32, height as i32)
|
||||
.get_image_data(0.0, 0.0, width as f64, height as f64)
|
||||
.ok()?;
|
||||
|
||||
let rgba_data = image_data.data().0;
|
||||
@@ -176,7 +174,6 @@ impl CanvasGlyphRenderer {
|
||||
image_data: rgba_data,
|
||||
width,
|
||||
height,
|
||||
advance_width,
|
||||
offset_x: -left as f32,
|
||||
offset_y: -ascent as f32,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#![expect(clippy::mem_forget)]
|
||||
|
||||
use ecolor::Color32;
|
||||
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
|
||||
use self_cell::self_cell;
|
||||
use skrifa::{
|
||||
MetadataProvider as _,
|
||||
raw::{TableProvider as _, tables::kern::SubtableKind},
|
||||
};
|
||||
use skrifa::{GlyphId, MetadataProvider as _};
|
||||
use std::collections::BTreeMap;
|
||||
use vello_cpu::{color, kurbo};
|
||||
|
||||
@@ -44,12 +42,10 @@ impl UvRect {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphInfo {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
///
|
||||
/// Is `None` for a special "invisible" glyph.
|
||||
pub(crate) id: Option<skrifa::GlyphId>,
|
||||
pub(crate) id: Option<GlyphId>,
|
||||
|
||||
/// In [`skrifa`]s "unscaled" coordinate system.
|
||||
pub advance_width_unscaled: OrderedFloat<f32>,
|
||||
@@ -63,6 +59,41 @@ impl GlyphInfo {
|
||||
};
|
||||
}
|
||||
|
||||
/// Result of resolving a `char` to a [`GlyphId`] within a single [`FontFace`].
|
||||
///
|
||||
/// Location-independent: only depends on the font's charmap and `FontTweak`,
|
||||
/// not on variable-font variation coordinates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(super) enum GlyphIdResolution {
|
||||
/// A real, visible glyph.
|
||||
Glyph(GlyphId),
|
||||
|
||||
/// A valid char, but rendered as zero-width (control chars, joiners, …).
|
||||
Invisible,
|
||||
}
|
||||
|
||||
/// A precomputed hash of a [`skrifa::instance::Location`].
|
||||
///
|
||||
/// Used as a cache key so that we don't have to re-hash the coordinate list
|
||||
/// for every glyph lookup. Compute once per text run and reuse for every glyph
|
||||
/// in the run.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct LocationHash(u64);
|
||||
|
||||
impl nohash_hasher::IsEnabled for LocationHash {}
|
||||
|
||||
impl LocationHash {
|
||||
#[inline]
|
||||
pub fn new(location: &skrifa::instance::Location) -> Self {
|
||||
if location.coords().is_empty() {
|
||||
// Fast path for the (common) default-coords case.
|
||||
Self(0)
|
||||
} else {
|
||||
Self(crate::util::hash(location))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subpixel binning, taken from cosmic-text:
|
||||
// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
|
||||
|
||||
@@ -124,17 +155,8 @@ impl SubpixelBin {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct GlyphAllocation {
|
||||
/// Used for pair-kerning.
|
||||
///
|
||||
/// Doesn't need to be unique.
|
||||
/// 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,
|
||||
|
||||
/// UV rectangle for drawing.
|
||||
pub uv_rect: UvRect,
|
||||
}
|
||||
@@ -145,10 +167,12 @@ struct GlyphCacheKey(u64);
|
||||
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
|
||||
|
||||
impl GlyphCacheKey {
|
||||
fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
#[inline]
|
||||
fn new(glyph_id: GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self {
|
||||
let StyledMetrics {
|
||||
pixels_per_point,
|
||||
px_scale_factor,
|
||||
location_hash,
|
||||
..
|
||||
} = *metrics;
|
||||
debug_assert!(
|
||||
@@ -164,6 +188,7 @@ impl GlyphCacheKey {
|
||||
pixels_per_point.to_bits(),
|
||||
px_scale_factor.to_bits(),
|
||||
bin,
|
||||
location_hash,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -175,7 +200,6 @@ struct DependentFontData<'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>,
|
||||
}
|
||||
|
||||
@@ -198,16 +222,14 @@ impl FontCell {
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: &GlyphInfo,
|
||||
glyph_id: GlyphId,
|
||||
bin: SubpixelBin,
|
||||
location: skrifa::instance::LocationRef<'_>,
|
||||
) -> Option<GlyphAllocation> {
|
||||
let glyph_id = glyph_info.id?;
|
||||
|
||||
// Return None for NOTDEF - this will trigger canvas fallback on WASM
|
||||
if glyph_id == skrifa::GlyphId::NOTDEF {
|
||||
return None;
|
||||
}
|
||||
debug_assert!(
|
||||
glyph_id != skrifa::GlyphId::NOTDEF,
|
||||
"Can't allocate glyph for id 0"
|
||||
);
|
||||
|
||||
let mut path = kurbo::BezPath::new();
|
||||
let mut pen = VelloPen {
|
||||
@@ -220,7 +242,9 @@ impl FontCell {
|
||||
|
||||
if let Some(hinting_instance) = &mut font_data.hinting_instance {
|
||||
let size = skrifa::instance::Size::new(metrics.scale);
|
||||
if hinting_instance.size() != size {
|
||||
if hinting_instance.size() != size
|
||||
|| hinting_instance.location().coords() != location.coords()
|
||||
{
|
||||
hinting_instance
|
||||
.reconfigure(
|
||||
&font_data.outline_glyphs,
|
||||
@@ -251,25 +275,31 @@ impl FontCell {
|
||||
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 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);
|
||||
let mut resources = vello_cpu::Resources::new();
|
||||
ctx.render_to_pixmap(&mut resources, &mut dest);
|
||||
|
||||
let glyph_pos = {
|
||||
let alpha_from_coverage = atlas.options().alpha_from_coverage;
|
||||
let color_transfer_function = atlas.options().color_transfer_function;
|
||||
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,
|
||||
);
|
||||
let pixel_offset = 4 * ((y * width as usize) + x);
|
||||
image[(x + glyph_pos.0, y + glyph_pos.1)] = color_transfer_function
|
||||
.to_atlas_color(Color32::from_rgba_premultiplied(
|
||||
pixels[pixel_offset],
|
||||
pixels[pixel_offset + 1],
|
||||
pixels[pixel_offset + 2],
|
||||
pixels[pixel_offset + 3],
|
||||
));
|
||||
}
|
||||
}
|
||||
glyph_pos
|
||||
@@ -288,11 +318,7 @@ impl FontCell {
|
||||
}
|
||||
};
|
||||
|
||||
Some(GlyphAllocation {
|
||||
id: glyph_id,
|
||||
advance_width_px: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor,
|
||||
uv_rect,
|
||||
})
|
||||
Some(GlyphAllocation { uv_rect })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,8 +362,24 @@ pub struct FontFace {
|
||||
name: String,
|
||||
font: FontCell,
|
||||
tweak: FontTweak,
|
||||
subpixel_binning: bool,
|
||||
|
||||
/// Cached `harfrust` shaper data (parsed GSUB/GPOS tables).
|
||||
/// `ShaperData` is `Copy` — lives outside the `self_cell`.
|
||||
shaper_data: harfrust::ShaperData,
|
||||
|
||||
/// Location-independent: `char → GlyphId | Invisible`.
|
||||
///
|
||||
/// Only depends on the font's charmap + `FontTweak`. A miss means the char
|
||||
/// is not in this face's repertoire and the fallback chain should be tried.
|
||||
glyph_id_cache: ahash::HashMap<char, GlyphIdResolution>,
|
||||
|
||||
/// Location-dependent: `(char, LocationHash) → unscaled advance width`.
|
||||
///
|
||||
/// Variable fonts can vary advance widths per axis (HVAR table), so this
|
||||
/// must be re-keyed per resolved [`skrifa::instance::Location`].
|
||||
advance_width_cache: ahash::HashMap<(char, LocationHash), OrderedFloat<f32>>,
|
||||
|
||||
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
|
||||
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
|
||||
|
||||
/// Cache for canvas-rendered glyphs (WASM only)
|
||||
@@ -364,16 +406,13 @@ impl FontFace {
|
||||
// Note: We use default location here during initialization because
|
||||
// the actual weight will be applied via the stored location during rendering.
|
||||
// The metrics won't be significantly different at this unscaled size.
|
||||
// TODO(emilk): heed location for vertical metrics too (HVAR/MVAR).
|
||||
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(),
|
||||
);
|
||||
|
||||
let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting);
|
||||
let hinting_enabled = tweak.hinting.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
|
||||
@@ -393,16 +432,22 @@ impl FontFace {
|
||||
charmap,
|
||||
outline_glyphs: glyphs,
|
||||
metrics,
|
||||
glyph_metrics,
|
||||
hinting_instance,
|
||||
})
|
||||
})?;
|
||||
|
||||
let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa);
|
||||
|
||||
let subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning);
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
font,
|
||||
tweak,
|
||||
glyph_info_cache: Default::default(),
|
||||
subpixel_binning,
|
||||
shaper_data,
|
||||
glyph_id_cache: Default::default(),
|
||||
advance_width_cache: Default::default(),
|
||||
glyph_alloc_cache: Default::default(),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
canvas_glyph_cache: Default::default(),
|
||||
@@ -438,104 +483,89 @@ impl FontFace {
|
||||
.filter_map(|(chr, _)| char::from_u32(chr).filter(|c| !self.ignore_character(*c)))
|
||||
}
|
||||
|
||||
/// `\n` will result in `None`
|
||||
pub(super) fn glyph_info(&mut self, c: char) -> Option<GlyphInfo> {
|
||||
if let Some(glyph_info) = self.glyph_info_cache.get(&c) {
|
||||
return Some(*glyph_info);
|
||||
/// Resolve a `char` to a [`GlyphId`] within this face.
|
||||
///
|
||||
/// Location-independent. Returns `None` when this face cannot represent
|
||||
/// the char (the caller should try the fallback chain).
|
||||
///
|
||||
/// `\t` and thin spaces share `' '`s glyph id (they just have a custom advance).
|
||||
pub(super) fn glyph_id_resolution(&mut self, c: char) -> Option<GlyphIdResolution> {
|
||||
if let Some(resolution) = self.glyph_id_cache.get(&c) {
|
||||
return Some(*resolution);
|
||||
}
|
||||
|
||||
if self.ignore_character(c) {
|
||||
return None; // these will result in the replacement character when rendering
|
||||
}
|
||||
|
||||
if c == '\t'
|
||||
&& let Some(space) = self.glyph_info(' ')
|
||||
{
|
||||
let glyph_info = GlyphInfo {
|
||||
advance_width_unscaled: (crate::text::TAB_SIZE as f32
|
||||
* space.advance_width_unscaled.0)
|
||||
.into(),
|
||||
..space
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
let resolution = if c == '\t' || c == '\u{2009}' || c == '\u{202F}' {
|
||||
// `\t` and thin spaces are rendered as a space glyph with a custom advance.
|
||||
self.glyph_id_resolution(' ')?
|
||||
} else if invisible_char(c) {
|
||||
GlyphIdResolution::Invisible
|
||||
} else {
|
||||
let glyph_id = self
|
||||
.font
|
||||
.borrow_dependent()
|
||||
.charmap
|
||||
.map(c)
|
||||
.filter(|id| *id != GlyphId::NOTDEF)?;
|
||||
GlyphIdResolution::Glyph(glyph_id)
|
||||
};
|
||||
|
||||
self.glyph_id_cache.insert(c, resolution);
|
||||
Some(resolution)
|
||||
}
|
||||
|
||||
/// Unscaled advance width for `c` at the given variation location.
|
||||
///
|
||||
/// Location-dependent (variable fonts can vary advances via HVAR).
|
||||
/// Cached per `(char, LocationHash)`.
|
||||
fn advance_width_unscaled(&mut self, c: char, metrics: &StyledMetrics) -> f32 {
|
||||
let cache_key = (c, metrics.location_hash);
|
||||
if let Some(advance) = self.advance_width_cache.get(&cache_key) {
|
||||
return advance.0;
|
||||
}
|
||||
|
||||
if c == '\u{2009}' {
|
||||
// Thin space, often used as thousands deliminator: 1 234 567 890
|
||||
// https://www.compart.com/en/unicode/U+2009
|
||||
// https://en.wikipedia.org/wiki/Thin_space
|
||||
|
||||
if let Some(space) = self.glyph_info(' ') {
|
||||
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(),
|
||||
..space
|
||||
};
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
let advance = match c {
|
||||
'\t' => self.tweak.tab_size * self.advance_width_unscaled(' ', metrics),
|
||||
'\u{2009}' | '\u{202F}' => {
|
||||
// Thin space (U+2009) and narrow no-break space (U+202F),
|
||||
// often used as thousands separator.
|
||||
self.tweak.thin_space_width * self.advance_width_unscaled(' ', metrics)
|
||||
}
|
||||
_ => {
|
||||
let Some(GlyphIdResolution::Glyph(glyph_id)) = self.glyph_id_resolution(c) else {
|
||||
return 0.0;
|
||||
};
|
||||
let font_data = self.font.borrow_dependent();
|
||||
let glyph_metrics = font_data
|
||||
.skrifa
|
||||
.glyph_metrics(skrifa::instance::Size::unscaled(), &metrics.location);
|
||||
glyph_metrics.advance_width(glyph_id).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
if invisible_char(c) {
|
||||
let glyph_info = GlyphInfo::INVISIBLE;
|
||||
self.glyph_info_cache.insert(c, glyph_info);
|
||||
return Some(glyph_info);
|
||||
}
|
||||
|
||||
let font_data = self.font.borrow_dependent();
|
||||
|
||||
// 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);
|
||||
|
||||
self.advance_width_cache.insert(cache_key, advance.into());
|
||||
advance
|
||||
}
|
||||
|
||||
/// `\n` will result in `None`.
|
||||
///
|
||||
/// Caller must pass [`StyledMetrics`] resolved against *this* face so that
|
||||
/// variable-font advance widths are looked up at the correct location.
|
||||
pub(super) fn glyph_info(&mut self, c: char, metrics: &StyledMetrics) -> Option<GlyphInfo> {
|
||||
let resolution = self.glyph_id_resolution(c)?;
|
||||
let glyph_info = match resolution {
|
||||
GlyphIdResolution::Invisible => GlyphInfo::INVISIBLE,
|
||||
GlyphIdResolution::Glyph(glyph_id) => GlyphInfo {
|
||||
id: Some(glyph_id),
|
||||
advance_width_unscaled: self.advance_width_unscaled(c, metrics).into(),
|
||||
},
|
||||
};
|
||||
Some(glyph_info)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn pair_kerning_pixels(
|
||||
&self,
|
||||
metrics: &StyledMetrics,
|
||||
last_glyph_id: skrifa::GlyphId,
|
||||
glyph_id: skrifa::GlyphId,
|
||||
) -> f32 {
|
||||
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: &StyledMetrics,
|
||||
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 styled_metrics(
|
||||
&self,
|
||||
@@ -559,13 +589,9 @@ impl FontFace {
|
||||
let axes = font_data.skrifa.axes();
|
||||
// Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the
|
||||
// argument (probably from TextFormat).
|
||||
let settings = self
|
||||
.tweak
|
||||
.coords
|
||||
.as_ref()
|
||||
.iter()
|
||||
.chain(coords.as_ref().iter());
|
||||
let settings = std::iter::chain(self.tweak.coords.as_ref(), coords.as_ref());
|
||||
let location = axes.location(settings);
|
||||
let location_hash = LocationHash::new(&location);
|
||||
|
||||
StyledMetrics {
|
||||
pixels_per_point,
|
||||
@@ -575,70 +601,81 @@ impl FontFace {
|
||||
ascent,
|
||||
row_height: ascent - descent + line_gap,
|
||||
location,
|
||||
location_hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn skrifa_font_ref(&self) -> &skrifa::FontRef<'_> {
|
||||
&self.font.borrow_dependent().skrifa
|
||||
}
|
||||
|
||||
pub(crate) fn tweak(&self) -> &FontTweak {
|
||||
&self.tweak
|
||||
}
|
||||
|
||||
pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData {
|
||||
&self.shaper_data
|
||||
}
|
||||
|
||||
pub fn allocate_glyph(
|
||||
&mut self,
|
||||
atlas: &mut TextureAtlas,
|
||||
metrics: &StyledMetrics,
|
||||
glyph_info: GlyphInfo,
|
||||
chr: char,
|
||||
h_pos: f32,
|
||||
shaped: &ShapedGlyph,
|
||||
) -> (GlyphAllocation, i32) {
|
||||
let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor;
|
||||
let ShapedGlyph {
|
||||
glyph_id,
|
||||
h_pos,
|
||||
is_cjk,
|
||||
..
|
||||
} = *shaped;
|
||||
|
||||
let Some(glyph_id) = glyph_info.id else {
|
||||
// Invisible.
|
||||
return (GlyphAllocation::default(), h_pos as i32);
|
||||
};
|
||||
|
||||
// CJK scripts contain a lot of characters and could hog the glyph atlas if we stored 4 subpixel offsets per
|
||||
// glyph.
|
||||
let (h_pos_round, bin) = if is_cjk(chr) {
|
||||
(h_pos.round() as i32, SubpixelBin::Zero)
|
||||
} else {
|
||||
let (h_pos_round, bin) = if self.subpixel_binning && !is_cjk {
|
||||
SubpixelBin::new(h_pos)
|
||||
} else {
|
||||
// CJK scripts contain a lot of characters and could hog the glyph atlas
|
||||
// if we stored 4 subpixel offsets per glyph.
|
||||
(h_pos.round() as i32, SubpixelBin::Zero)
|
||||
};
|
||||
|
||||
// For canvas glyphs (NOTDEF), we need to use the character for caching
|
||||
// because all canvas glyphs have the same glyph_id
|
||||
// On WASM, render glyphs the font can't provide (NOTDEF) via an HTML canvas.
|
||||
// This is a last resort: it only runs after the shaper and the `resolve_face`
|
||||
// font fallback have both failed to map the character to a real glyph.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if glyph_id == skrifa::GlyphId::NOTDEF {
|
||||
// Use character-based cache key for canvas glyphs
|
||||
// Include bin for subpixel positioning
|
||||
let canvas_cache_key = (chr, metrics.pixels_per_point.to_bits(), metrics.px_scale_factor.to_bits(), bin);
|
||||
|
||||
// Check canvas glyph cache
|
||||
if glyph_id == GlyphId::NOTDEF {
|
||||
// All canvas glyphs share the NOTDEF glyph id, so we can't key the
|
||||
// regular glyph cache on it. Key on the character (plus scale and
|
||||
// subpixel bin) instead.
|
||||
let canvas_cache_key = (
|
||||
shaped.chr,
|
||||
metrics.pixels_per_point.to_bits(),
|
||||
metrics.px_scale_factor.to_bits(),
|
||||
bin,
|
||||
);
|
||||
if let Some(&cached_alloc) = self.canvas_glyph_cache.get(&canvas_cache_key) {
|
||||
return (cached_alloc, h_pos_round);
|
||||
}
|
||||
|
||||
// Render with canvas at subpixel offset
|
||||
let allocation = Self::try_allocate_canvas_glyph_static(atlas, metrics, chr, bin).unwrap_or_default();
|
||||
|
||||
// Cache it
|
||||
let allocation =
|
||||
Self::try_allocate_canvas_glyph_static(atlas, metrics, shaped.chr, bin)
|
||||
.unwrap_or_default();
|
||||
self.canvas_glyph_cache.insert(canvas_cache_key, allocation);
|
||||
return (allocation, h_pos_round);
|
||||
}
|
||||
|
||||
// Check cache first (for non-canvas glyphs)
|
||||
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
|
||||
if let Some(&cached_alloc) = self.glyph_alloc_cache.get(&cache_key) {
|
||||
let mut glyph_alloc = cached_alloc;
|
||||
glyph_alloc.advance_width_px = advance_width_px; // Hack to get `\t` and thin space to work, since they use the same glyph id as ` ` (space).
|
||||
return (glyph_alloc, h_pos_round);
|
||||
if glyph_id == GlyphId::NOTDEF {
|
||||
// invisible
|
||||
return (GlyphAllocation::default(), h_pos_round);
|
||||
}
|
||||
|
||||
// Allocate the glyph
|
||||
let allocation = self
|
||||
.font
|
||||
.allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into())
|
||||
.unwrap_or_default();
|
||||
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
|
||||
|
||||
// Insert into cache
|
||||
self.glyph_alloc_cache.insert(cache_key, allocation);
|
||||
(allocation, h_pos_round)
|
||||
let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| {
|
||||
self.font
|
||||
.allocate_glyph_uncached(atlas, metrics, glyph_id, bin, (&metrics.location).into())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
(alloc, h_pos_round)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -662,6 +699,25 @@ impl FontFace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Positioning info for a single glyph, ready for atlas allocation.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ShapedGlyph {
|
||||
pub glyph_id: GlyphId,
|
||||
|
||||
/// The character this glyph was shaped from.
|
||||
///
|
||||
/// Used by the WASM canvas fallback to render glyphs missing from all loaded
|
||||
/// fonts (where `glyph_id` is [`GlyphId::NOTDEF`]).
|
||||
#[cfg_attr(not(target_arch = "wasm32"), expect(dead_code))]
|
||||
pub chr: char,
|
||||
|
||||
/// Horizontal position of the glyph origin, in physical pixels.
|
||||
pub h_pos: f32,
|
||||
|
||||
/// CJK glyphs skip subpixel positioning to save atlas space.
|
||||
pub is_cjk: bool,
|
||||
}
|
||||
|
||||
// TODO(emilk): rename?
|
||||
/// Wrapper over multiple [`FontFace`] (e.g. a primary + fallbacks for emojis)
|
||||
pub struct Font<'a> {
|
||||
@@ -673,7 +729,7 @@ pub struct Font<'a> {
|
||||
impl Font<'_> {
|
||||
pub fn preload_characters(&mut self, s: &str) {
|
||||
for c in s.chars() {
|
||||
self.glyph_info(c);
|
||||
self.resolve_face(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,19 +761,23 @@ impl Font<'_> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Width of this character in points.
|
||||
/// Width of this character in points, at the font's default variation location.
|
||||
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.font.px_scale_factor(font_size)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
let face_key = self.resolve_face(c);
|
||||
let Some(font_face) = self.fonts_by_id.get_mut(&face_key) else {
|
||||
return 0.0;
|
||||
};
|
||||
let metrics = font_face.styled_metrics(1.0, font_size, &VariationCoords::default());
|
||||
let Some(glyph_info) = font_face.glyph_info(c, &metrics) else {
|
||||
return 0.0;
|
||||
};
|
||||
glyph_info.advance_width_unscaled.0 * font_face.font.px_scale_factor(font_size)
|
||||
}
|
||||
|
||||
/// Can we display this glyph?
|
||||
pub fn has_glyph(&mut self, c: char) -> bool {
|
||||
self.glyph_info(c) != self.cached_family.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦♂️
|
||||
// TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦♂️
|
||||
self.resolve_face(c) != self.cached_family.replacement_face_key
|
||||
}
|
||||
|
||||
/// Can we display all the glyphs in this text?
|
||||
@@ -725,21 +785,52 @@ impl Font<'_> {
|
||||
s.chars().all(|c| self.has_glyph(c))
|
||||
}
|
||||
|
||||
/// `\n` will (intentionally) show up as the replacement character.
|
||||
pub(crate) fn glyph_info(&mut self, c: char) -> (FontFaceKey, GlyphInfo) {
|
||||
if let Some(font_index_glyph_info) = self.cached_family.glyph_info_cache.get(&c) {
|
||||
return *font_index_glyph_info;
|
||||
/// Find which face in the fallback chain owns `c`.
|
||||
///
|
||||
/// Location-independent — fallback choice depends only on charmap support.
|
||||
/// Falls back to the replacement-glyph face when no fallback face has `c`.
|
||||
#[inline]
|
||||
pub(crate) fn resolve_face(&mut self, c: char) -> FontFaceKey {
|
||||
if let Some(font_key) = self.cached_family.face_cache.get(&c) {
|
||||
return *font_key;
|
||||
}
|
||||
self.resolve_face_slow(c)
|
||||
}
|
||||
|
||||
let font_index_glyph_info = self
|
||||
#[cold]
|
||||
fn resolve_face_slow(&mut self, c: char) -> FontFaceKey {
|
||||
let font_key = self
|
||||
.cached_family
|
||||
.glyph_info_no_cache_or_fallback(c, self.fonts_by_id);
|
||||
let font_index_glyph_info =
|
||||
font_index_glyph_info.unwrap_or(self.cached_family.replacement_glyph);
|
||||
self.cached_family
|
||||
.glyph_info_cache
|
||||
.insert(c, font_index_glyph_info);
|
||||
font_index_glyph_info
|
||||
.find_face_for_char(c, self.fonts_by_id)
|
||||
.unwrap_or(self.cached_family.replacement_face_key);
|
||||
self.cached_family.face_cache.insert(c, font_key);
|
||||
font_key
|
||||
}
|
||||
|
||||
/// Resolve `c` to its (face, [`GlyphInfo`]) at the given face's location.
|
||||
///
|
||||
/// `\n` will (intentionally) show up as the replacement character.
|
||||
///
|
||||
/// `metrics` must be the resolved [`StyledMetrics`] for the face that ends
|
||||
/// up owning `c`. Most callers pass the metrics of their text run's primary
|
||||
/// face — that is correct as long as `c` is in that face. For correct
|
||||
/// fallback-face advances, resolve the face first with [`Self::resolve_face`]
|
||||
/// and build metrics for that face.
|
||||
pub(crate) fn glyph_info(
|
||||
&mut self,
|
||||
c: char,
|
||||
metrics: &StyledMetrics,
|
||||
) -> (FontFaceKey, GlyphInfo) {
|
||||
let face_key = self.resolve_face(c);
|
||||
let Some(face) = self.fonts_by_id.get_mut(&face_key) else {
|
||||
return (face_key, GlyphInfo::INVISIBLE);
|
||||
};
|
||||
let glyph_info = face.glyph_info(c, metrics).unwrap_or_else(|| {
|
||||
// `c` is in no face — render the replacement character instead.
|
||||
face.glyph_info(self.cached_family.replacement_char, metrics)
|
||||
.unwrap_or(GlyphInfo::INVISIBLE)
|
||||
});
|
||||
(face_key, glyph_info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,6 +863,12 @@ pub struct StyledMetrics {
|
||||
|
||||
/// Resolved variation coordinates.
|
||||
pub location: skrifa::instance::Location,
|
||||
|
||||
/// Precomputed hash of [`Self::location`].
|
||||
///
|
||||
/// Hashed once per run of text so per-glyph cache lookups don't have to
|
||||
/// re-hash the full coordinate list.
|
||||
pub(crate) location_hash: LocationHash,
|
||||
}
|
||||
|
||||
/// Code points that will always be invisible (zero width).
|
||||
@@ -835,36 +932,31 @@ pub(super) fn allocate_canvas_glyph(
|
||||
image_data,
|
||||
width,
|
||||
height,
|
||||
advance_width,
|
||||
offset_x,
|
||||
offset_y,
|
||||
} = canvas_data;
|
||||
|
||||
// Get alpha_from_coverage before allocating
|
||||
let alpha_from_coverage = atlas.options().alpha_from_coverage;
|
||||
// How to convert the canvas coverage into atlas colors.
|
||||
let color_transfer_function = atlas.options().color_transfer_function;
|
||||
|
||||
// Allocate space in the texture atlas
|
||||
let (glyph_pos, image) = atlas.allocate((width as usize, height as usize));
|
||||
|
||||
// Convert RGBA to alpha channel
|
||||
// Canvas ImageData is RGBA, we need alpha channel only
|
||||
// Canvas `ImageData` is RGBA; we only need the alpha (coverage) channel.
|
||||
for y in 0..height as usize {
|
||||
for x in 0..width as usize {
|
||||
let idx = (y * width as usize + x) * 4;
|
||||
// Use alpha channel from ImageData
|
||||
let alpha = image_data[idx + 3] as f32 / 255.0;
|
||||
image[(x + glyph_pos.0, y + glyph_pos.1)] =
|
||||
alpha_from_coverage.color_from_coverage(alpha);
|
||||
color_transfer_function.color_from_coverage(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate offset in points
|
||||
let offset_in_points =
|
||||
Vec2::new(offset_x, offset_y) / metrics.pixels_per_point + metrics.y_offset_in_points * Vec2::Y;
|
||||
let offset_in_points = Vec2::new(offset_x, offset_y) / metrics.pixels_per_point
|
||||
+ metrics.y_offset_in_points * Vec2::Y;
|
||||
|
||||
GlyphAllocation {
|
||||
id: skrifa::GlyphId::NOTDEF, // Mark as canvas-rendered
|
||||
advance_width_px: advance_width,
|
||||
uv_rect: UvRect {
|
||||
offset: offset_in_points,
|
||||
size: Vec2::new(width as f32, height as f32) / metrics.pixels_per_point,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
TextureAtlas,
|
||||
text::{
|
||||
Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
|
||||
font::{Font, FontFace, GlyphInfo},
|
||||
font::{Font, FontFace},
|
||||
},
|
||||
};
|
||||
use emath::{NumExt as _, OrderedFloat};
|
||||
@@ -188,11 +188,29 @@ pub struct FontTweak {
|
||||
|
||||
/// Override the global font hinting setting for this specific font.
|
||||
///
|
||||
/// `None` means use the global setting.
|
||||
pub hinting_override: Option<bool>,
|
||||
/// `None` means use the global setting in [`TextOptions::font_hinting`].
|
||||
pub hinting: Option<bool>,
|
||||
|
||||
/// Override the global sub-pixel binning setting for this specific font.
|
||||
///
|
||||
/// `None` means use the global setting in [`TextOptions::subpixel_binning`].
|
||||
pub subpixel_binning: Option<bool>,
|
||||
|
||||
/// Override the font's default variation coordinates.
|
||||
pub coords: VariationCoords,
|
||||
|
||||
/// Width of a thin space (`\u{2009}`) and narrow no-break space (`\u{202F}`),
|
||||
/// as a fraction of the normal space width.
|
||||
///
|
||||
/// Thin space is often used as a thousands separator: `1 234 567`.
|
||||
///
|
||||
/// Default: `0.5` (half a normal space).
|
||||
pub thin_space_width: f32,
|
||||
|
||||
/// Width of a tab character (`\t`), measured in number of space widths.
|
||||
///
|
||||
/// Default: `4.0`.
|
||||
pub tab_size: f32,
|
||||
}
|
||||
|
||||
impl Default for FontTweak {
|
||||
@@ -201,8 +219,11 @@ impl Default for FontTweak {
|
||||
scale: 1.0,
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
hinting_override: None,
|
||||
hinting: None,
|
||||
subpixel_binning: None,
|
||||
coords: VariationCoords::default(),
|
||||
thin_space_width: 0.5,
|
||||
tab_size: 4.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +439,7 @@ impl FontFaceKey {
|
||||
pub const INVALID: Self = Self(0);
|
||||
|
||||
fn new() -> Self {
|
||||
static KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
static KEY_COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
Self(crate::util::hash(
|
||||
KEY_COUNTER.fetch_add(1, Ordering::Relaxed),
|
||||
))
|
||||
@@ -436,9 +457,20 @@ pub(super) struct CachedFamily {
|
||||
/// Lazily calculated.
|
||||
pub characters: Option<BTreeMap<char, Vec<String>>>,
|
||||
|
||||
pub replacement_glyph: (FontFaceKey, GlyphInfo),
|
||||
/// The face used when no face in [`Self::fonts`] supports a char.
|
||||
pub replacement_face_key: FontFaceKey,
|
||||
|
||||
pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
|
||||
/// The char that [`Self::replacement_face_key`] actually contains.
|
||||
///
|
||||
/// When the user asks about a char that no fallback face supports we
|
||||
/// render this char in its place.
|
||||
pub replacement_char: char,
|
||||
|
||||
/// Cache: `char → which face in the fallback chain owns this char`.
|
||||
///
|
||||
/// Location-independent (fallback choice depends only on charmap support,
|
||||
/// not on variation coordinates).
|
||||
pub face_cache: ahash::HashMap<char, FontFaceKey>,
|
||||
}
|
||||
|
||||
impl CachedFamily {
|
||||
@@ -446,68 +478,65 @@ impl CachedFamily {
|
||||
fonts: Vec<FontFaceKey>,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Self {
|
||||
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
|
||||
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
|
||||
|
||||
if fonts.is_empty() {
|
||||
return Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
|
||||
glyph_info_cache: Default::default(),
|
||||
replacement_face_key: FontFaceKey::INVALID,
|
||||
replacement_char: PRIMARY_REPLACEMENT_CHAR,
|
||||
face_cache: Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut slf = Self {
|
||||
fonts,
|
||||
characters: None,
|
||||
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
|
||||
glyph_info_cache: Default::default(),
|
||||
replacement_face_key: FontFaceKey::INVALID,
|
||||
replacement_char: PRIMARY_REPLACEMENT_CHAR,
|
||||
face_cache: Default::default(),
|
||||
};
|
||||
|
||||
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
|
||||
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
|
||||
|
||||
let replacement_glyph = slf
|
||||
.glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
|
||||
.or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR, fonts_by_id))
|
||||
let (replacement_face_key, replacement_char) = slf
|
||||
.find_face_for_char(PRIMARY_REPLACEMENT_CHAR, fonts_by_id)
|
||||
.map(|key| (key, PRIMARY_REPLACEMENT_CHAR))
|
||||
.or_else(|| {
|
||||
slf.find_face_for_char(FALLBACK_REPLACEMENT_CHAR, fonts_by_id)
|
||||
.map(|key| (key, FALLBACK_REPLACEMENT_CHAR))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
|
||||
);
|
||||
(FontFaceKey::INVALID, GlyphInfo::INVISIBLE)
|
||||
(FontFaceKey::INVALID, PRIMARY_REPLACEMENT_CHAR)
|
||||
});
|
||||
slf.replacement_glyph = replacement_glyph;
|
||||
slf.replacement_face_key = replacement_face_key;
|
||||
slf.replacement_char = replacement_char;
|
||||
|
||||
slf
|
||||
}
|
||||
|
||||
pub(crate) fn glyph_info_no_cache_or_fallback(
|
||||
&mut self,
|
||||
/// Walk the fallback chain and return the first face whose charmap supports `c`.
|
||||
///
|
||||
/// Pure — does not touch any cache. Callers that want memoisation should
|
||||
/// insert into [`Self::face_cache`] themselves.
|
||||
pub(crate) fn find_face_for_char(
|
||||
&self,
|
||||
c: char,
|
||||
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
) -> Option<(FontFaceKey, GlyphInfo)> {
|
||||
) -> Option<FontFaceKey> {
|
||||
for font_key in &self.fonts {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas fallback for WASM
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// Use a special GlyphInfo to indicate canvas rendering is needed
|
||||
// We use NOTDEF as the glyph ID to mark this as a canvas glyph
|
||||
// The advance width will be filled in when we actually render it
|
||||
let canvas_glyph_info = GlyphInfo {
|
||||
id: Some(skrifa::GlyphId::NOTDEF),
|
||||
advance_width_unscaled: OrderedFloat(0.0), // Will be filled in during rendering
|
||||
};
|
||||
// Use first font key as placeholder (the actual font family list will be used during rendering)
|
||||
if let Some(&first_font_key) = self.fonts.first() {
|
||||
return Some((first_font_key, canvas_glyph_info));
|
||||
if font_face.glyph_id_resolution(c).is_some() {
|
||||
return Some(*font_key);
|
||||
}
|
||||
}
|
||||
|
||||
// No loaded font contains `c`. On WASM the caller falls back to rendering
|
||||
// it through an HTML canvas (see the NOTDEF handling in `text_layout`);
|
||||
// elsewhere it shows the replacement character.
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -782,6 +811,9 @@ pub struct FontsImpl {
|
||||
fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontFace>,
|
||||
fonts_by_name: ahash::HashMap<String, FontFaceKey>,
|
||||
family_cache: ahash::HashMap<FontFamily, CachedFamily>,
|
||||
|
||||
/// Recycled `harfrust` shaping buffer to avoid per-layout allocations.
|
||||
shape_buffer: Option<harfrust::UnicodeBuffer>,
|
||||
}
|
||||
|
||||
impl FontsImpl {
|
||||
@@ -815,6 +847,7 @@ impl FontsImpl {
|
||||
fonts_by_id,
|
||||
fonts_by_name,
|
||||
family_cache: Default::default(),
|
||||
shape_buffer: Some(harfrust::UnicodeBuffer::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,6 +855,16 @@ impl FontsImpl {
|
||||
self.atlas.options()
|
||||
}
|
||||
|
||||
/// Take the recycled shaping buffer (or create a new one if already taken).
|
||||
pub fn take_shape_buffer(&mut self) -> harfrust::UnicodeBuffer {
|
||||
self.shape_buffer.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Return a shaping buffer for reuse.
|
||||
pub fn return_shape_buffer(&mut self, buffer: harfrust::UnicodeBuffer) {
|
||||
self.shape_buffer = Some(buffer);
|
||||
}
|
||||
|
||||
/// 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(|| {
|
||||
@@ -1017,6 +1060,7 @@ impl GalleyCache {
|
||||
0.0
|
||||
},
|
||||
round_output_to_gui: job.round_output_to_gui,
|
||||
keep_trailing_whitespace: job.keep_trailing_whitespace,
|
||||
};
|
||||
|
||||
// Add overlapping sections:
|
||||
|
||||
@@ -31,8 +31,8 @@ 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,
|
||||
/// Controls how to convert glyph colors when writing to the font atlas.
|
||||
pub color_transfer_function: crate::FontColorTransferFunction,
|
||||
|
||||
/// Whether to enable font hinting
|
||||
///
|
||||
@@ -40,14 +40,29 @@ pub struct TextOptions {
|
||||
///
|
||||
/// Default is `true`.
|
||||
pub font_hinting: bool,
|
||||
|
||||
/// Enable sub-pixel binning for glyphs.
|
||||
///
|
||||
/// Sub-pixel binning renders each glyph at up to four fractional horizontal offsets,
|
||||
/// giving more even kerning at the cost of more atlas space.
|
||||
///
|
||||
/// It also lead to text looking more blurry.
|
||||
///
|
||||
/// This is always disabled for CJK characters (which have too many unique glyphs).
|
||||
///
|
||||
/// Can be overridden per font with [`FontTweak::subpixel_binning`].
|
||||
///
|
||||
/// Default: `true`.
|
||||
pub subpixel_binning: bool,
|
||||
}
|
||||
|
||||
impl Default for TextOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_texture_side: 2048, // Small but portable
|
||||
alpha_from_coverage: crate::AlphaFromCoverage::default(),
|
||||
color_transfer_function: crate::FontColorTransferFunction::default(),
|
||||
font_hinting: true,
|
||||
subpixel_binning: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,14 @@ pub struct LayoutJob {
|
||||
|
||||
/// Round output sizes using [`emath::GuiRounding`], to avoid rounding errors in layout code.
|
||||
pub round_output_to_gui: bool,
|
||||
|
||||
/// If `false` (default), trailing whitespace is ignored when computing
|
||||
/// horizontal alignment ([`Self::halign`]).
|
||||
/// This is desirable for labels so that e.g. "Hello " centers the same as "Hello".
|
||||
///
|
||||
/// If `true`, trailing whitespace is included in the row width used for alignment.
|
||||
/// This is desirable for text editors where the user expects to see their spaces.
|
||||
pub keep_trailing_whitespace: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutJob {
|
||||
@@ -93,6 +101,7 @@ impl Default for LayoutJob {
|
||||
halign: Align::LEFT,
|
||||
justify: false,
|
||||
round_output_to_gui: true,
|
||||
keep_trailing_whitespace: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +225,7 @@ impl std::hash::Hash for LayoutJob {
|
||||
halign,
|
||||
justify,
|
||||
round_output_to_gui,
|
||||
keep_trailing_whitespace,
|
||||
} = self;
|
||||
|
||||
text.hash(state);
|
||||
@@ -226,6 +236,7 @@ impl std::hash::Hash for LayoutJob {
|
||||
halign.hash(state);
|
||||
justify.hash(state);
|
||||
round_output_to_gui.hash(state);
|
||||
keep_trailing_whitespace.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,9 +707,12 @@ impl PlacedRow {
|
||||
|
||||
/// Same as [`Self::rect`] but excluding the `LayoutSection::leading_space`.
|
||||
pub fn rect_without_leading_space(&self) -> Rect {
|
||||
let x = self.glyphs.first().map_or(self.pos.x, |g| g.pos.x);
|
||||
let size_x = self.size.x - x;
|
||||
Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y))
|
||||
let x = self.pos.x + self.glyphs.first().map_or(0.0, |g| g.pos.x);
|
||||
let right = self.pos.x + self.size.x;
|
||||
Rect::from_min_max(
|
||||
Pos2::new(x, self.pos.y),
|
||||
Pos2::new(right, self.pos.y + self.size.y),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,7 +1061,7 @@ impl Galley {
|
||||
return self.end_pos();
|
||||
};
|
||||
|
||||
let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left();
|
||||
let x = row.x_offset(layout_cursor.column) + row.pos.x;
|
||||
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
|
||||
}
|
||||
|
||||
@@ -1092,7 +1106,7 @@ impl Galley {
|
||||
if is_pos_within_row || y_dist < best_y_dist {
|
||||
best_y_dist = y_dist;
|
||||
// char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos.
|
||||
let column = row.char_at(pos.x - row.pos.x + self.rect.left());
|
||||
let column = row.char_at(pos.x - row.pos.x);
|
||||
let prefer_next_row = column < row.char_count_excluding_newline();
|
||||
cursor = CCursor {
|
||||
index: ccursor_index + column,
|
||||
@@ -1244,7 +1258,8 @@ impl Galley {
|
||||
|
||||
let new_layout_cursor = {
|
||||
// keep same X coord
|
||||
let column = self.rows[new_row].char_at(h_pos);
|
||||
// char_at is Row-relative, so subtract the row's position
|
||||
let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
|
||||
LayoutCursor {
|
||||
row: new_row,
|
||||
column,
|
||||
@@ -1266,7 +1281,8 @@ impl Galley {
|
||||
|
||||
let new_layout_cursor = {
|
||||
// keep same X coord
|
||||
let column = self.rows[new_row].char_at(h_pos);
|
||||
// char_at is Row-relative, so subtract the row's position
|
||||
let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
|
||||
LayoutCursor {
|
||||
row: new_row,
|
||||
column,
|
||||
|
||||
@@ -120,8 +120,9 @@ impl TextureAtlas {
|
||||
let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
|
||||
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)] =
|
||||
options.alpha_from_coverage.color_from_coverage(coverage);
|
||||
image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] = options
|
||||
.color_transfer_function
|
||||
.color_from_coverage(coverage);
|
||||
}
|
||||
}
|
||||
atlas.discs.push(PrerasterizedDisc {
|
||||
|
||||
Reference in New Issue
Block a user