1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

More even text kerning (#7431)

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
valadaptive
2025-09-08 11:29:41 -04:00
committed by GitHub
parent e5d0b93633
commit d5b0a6f446
162 changed files with 1203 additions and 1131 deletions

View File

@@ -1,12 +1,14 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use emath::{GuiRounding as _, Vec2, vec2};
use ab_glyph::{Font as _, OutlinedGlyph, PxScale};
use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2};
use crate::{
TextureAtlas,
mutex::{Mutex, RwLock},
text::FontTweak,
text::{
FontTweak,
fonts::{CachedFamily, FontFaceKey},
},
};
// ----------------------------------------------------------------------------
@@ -34,108 +36,168 @@ impl UvRect {
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[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<ab_glyph::GlyphId>,
/// In [`ab_glyph`]s "unscaled" coordinate system.
pub advance_width_unscaled: OrderedFloat<f32>,
}
impl GlyphInfo {
/// A valid, but invisible, glyph of zero-width.
pub const INVISIBLE: Self = Self {
id: None,
advance_width_unscaled: OrderedFloat(0.0),
};
}
// Subpixel binning, taken from cosmic-text:
// https://github.com/pop-os/cosmic-text/blob/974ddaed96b334f560b606ebe5d2ca2d2f9f23ef/src/glyph_cache.rs
/// Bin for subpixel positioning of glyphs.
///
/// For accurate glyph positioning, we want to render each glyph at a subpixel coordinate. However, we also want to
/// cache each glyph's bitmap. As a compromise, we bin each subpixel offset into one of four fractional values. This
/// means one glyph can have up to four subpixel-positioned bitmaps in the cache.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub(super) enum SubpixelBin {
#[default]
Zero,
One,
Two,
Three,
}
impl SubpixelBin {
/// Bin the given position and return the new integral coordinate.
fn new(pos: f32) -> (i32, Self) {
let trunc = pos as i32;
let fract = pos - trunc as f32;
#[expect(clippy::collapsible_else_if)]
if pos.is_sign_negative() {
if fract > -0.125 {
(trunc, Self::Zero)
} else if fract > -0.375 {
(trunc - 1, Self::Three)
} else if fract > -0.625 {
(trunc - 1, Self::Two)
} else if fract > -0.875 {
(trunc - 1, Self::One)
} else {
(trunc - 1, Self::Zero)
}
} else {
if fract < 0.125 {
(trunc, Self::Zero)
} else if fract < 0.375 {
(trunc, Self::One)
} else if fract < 0.625 {
(trunc, Self::Two)
} else if fract < 0.875 {
(trunc, Self::Three)
} else {
(trunc + 1, Self::Zero)
}
}
}
pub fn as_float(&self) -> f32 {
match self {
Self::Zero => 0.0,
Self::One => 0.25,
Self::Two => 0.5,
Self::Three => 0.75,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GlyphAllocation {
/// Used for pair-kerning.
///
/// 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,
/// Unit: points.
pub advance_width: f32,
/// Unit: screen pixels.
pub advance_width_px: f32,
/// Texture coordinates.
/// UV rectangle for drawing.
pub uv_rect: UvRect,
}
impl Default for GlyphInfo {
/// Basically a zero-width space.
fn default() -> Self {
Self {
id: ab_glyph::GlyphId(0),
advance_width: 0.0,
uv_rect: Default::default(),
}
#[derive(Hash, PartialEq, Eq)]
struct GlyphCacheKey(u64);
impl nohash_hasher::IsEnabled for GlyphCacheKey {}
impl GlyphCacheKey {
fn new(glyph_id: ab_glyph::GlyphId, metrics: &ScaledMetrics, bin: SubpixelBin) -> Self {
let ScaledMetrics {
pixels_per_point,
px_scale_factor,
..
} = *metrics;
debug_assert!(
0.0 < pixels_per_point && pixels_per_point.is_finite(),
"Bad pixels_per_point {pixels_per_point}"
);
debug_assert!(
0.0 < px_scale_factor && px_scale_factor.is_finite(),
"Bad px_scale_factor: {px_scale_factor}"
);
Self(crate::util::hash((
glyph_id,
pixels_per_point.to_bits(),
px_scale_factor.to_bits(),
bin,
)))
}
}
// ----------------------------------------------------------------------------
/// A specific font with a size.
/// A specific font face.
/// The interface uses points as the unit for everything.
pub struct FontImpl {
name: String,
ab_glyph_font: ab_glyph::FontArc,
tweak: FontTweak,
glyph_info_cache: ahash::HashMap<char, GlyphInfo>,
glyph_alloc_cache: ahash::HashMap<GlyphCacheKey, GlyphAllocation>,
}
/// Maximum character height
scale_in_pixels: u32,
trait FontExt {
fn px_scale_factor(&self, scale: f32) -> f32;
}
height_in_points: f32,
// move each character by this much (hack)
y_offset_in_points: f32,
ascent: f32,
pixels_per_point: f32,
glyph_info_cache: RwLock<ahash::HashMap<char, GlyphInfo>>, // TODO(emilk): standard Mutex
atlas: Arc<Mutex<TextureAtlas>>,
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
}
}
impl FontImpl {
pub fn new(
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
name: String,
ab_glyph_font: ab_glyph::FontArc,
scale_in_pixels: f32,
tweak: FontTweak,
) -> Self {
assert!(
scale_in_pixels > 0.0,
"scale_in_pixels is smaller than 0, got: {scale_in_pixels:?}"
);
assert!(
pixels_per_point > 0.0,
"pixels_per_point must be greater than 0, got: {pixels_per_point:?}"
);
use ab_glyph::{Font as _, ScaleFont as _};
let scaled = ab_glyph_font.as_scaled(scale_in_pixels);
let ascent = (scaled.ascent() / pixels_per_point).round_ui();
let descent = (scaled.descent() / pixels_per_point).round_ui();
let line_gap = (scaled.line_gap() / pixels_per_point).round_ui();
// Tweak the scale as the user desired
let scale_in_pixels = scale_in_pixels * tweak.scale;
let scale_in_points = scale_in_pixels / pixels_per_point;
let baseline_offset = (scale_in_points * tweak.baseline_offset_factor).round_ui();
let y_offset_points =
((scale_in_points * tweak.y_offset_factor) + tweak.y_offset).round_ui();
// Center scaled glyphs properly:
let height = ascent + descent;
let y_offset_points = y_offset_points - (1.0 - tweak.scale) * 0.5 * height;
// Round to an even number of physical pixels to get even kerning.
// See https://github.com/emilk/egui/issues/382
let scale_in_pixels = scale_in_pixels.round() as u32;
// Round to closest pixel:
let y_offset_in_points = (y_offset_points * pixels_per_point).round() / pixels_per_point;
pub fn new(name: String, ab_glyph_font: ab_glyph::FontArc, tweak: FontTweak) -> Self {
Self {
name,
ab_glyph_font,
scale_in_pixels,
height_in_points: ascent - descent + line_gap,
y_offset_in_points,
ascent: ascent + baseline_offset,
pixels_per_point,
tweak,
glyph_info_cache: Default::default(),
atlas,
glyph_alloc_cache: Default::default(),
}
}
@@ -161,7 +223,6 @@ impl FontImpl {
/// An un-ordered iterator over all supported characters.
fn characters(&self) -> impl Iterator<Item = char> + '_ {
use ab_glyph::Font as _;
self.ab_glyph_font
.codepoint_ids()
.map(|(_, chr)| chr)
@@ -169,11 +230,9 @@ impl FontImpl {
}
/// `\n` will result in `None`
fn glyph_info(&self, c: char) -> Option<GlyphInfo> {
{
if let Some(glyph_info) = self.glyph_info_cache.read().get(&c) {
return Some(*glyph_info);
}
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);
}
if self.ignore_character(c) {
@@ -183,10 +242,12 @@ impl FontImpl {
if c == '\t' {
if let Some(space) = self.glyph_info(' ') {
let glyph_info = GlyphInfo {
advance_width: crate::text::TAB_SIZE as f32 * space.advance_width,
advance_width_unscaled: (crate::text::TAB_SIZE as f32
* space.advance_width_unscaled.0)
.into(),
..space
};
self.glyph_info_cache.write().insert(c, glyph_info);
self.glyph_info_cache.insert(c, glyph_info);
return Some(glyph_info);
}
}
@@ -197,91 +258,150 @@ impl FontImpl {
// https://en.wikipedia.org/wiki/Thin_space
if let Some(space) = self.glyph_info(' ') {
let em = self.height_in_points; // TODO(emilk): is this right?
let advance_width = f32::min(em / 6.0, space.advance_width * 0.5);
let em = self.ab_glyph_font.units_per_em().unwrap_or(1.0);
let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5);
let glyph_info = GlyphInfo {
advance_width,
advance_width_unscaled: advance_width.into(),
..space
};
self.glyph_info_cache.write().insert(c, glyph_info);
self.glyph_info_cache.insert(c, glyph_info);
return Some(glyph_info);
}
}
if invisible_char(c) {
let glyph_info = GlyphInfo::default();
self.glyph_info_cache.write().insert(c, glyph_info);
let glyph_info = GlyphInfo::INVISIBLE;
self.glyph_info_cache.insert(c, glyph_info);
return Some(glyph_info);
}
// Add new character:
use ab_glyph::Font as _;
let glyph_id = self.ab_glyph_font.glyph_id(c);
if glyph_id.0 == 0 {
None // unsupported character
} else {
let glyph_info = self.allocate_glyph(glyph_id);
self.glyph_info_cache.write().insert(c, glyph_info);
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)
}
}
#[inline]
pub fn pair_kerning(
pub(super) fn pair_kerning_pixels(
&self,
metrics: &ScaledMetrics,
last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId,
) -> f32 {
use ab_glyph::{Font as _, ScaleFont as _};
self.ab_glyph_font
.as_scaled(self.scale_in_pixels as f32)
.kern(last_glyph_id, glyph_id)
/ self.pixels_per_point
self.ab_glyph_font.kern_unscaled(last_glyph_id, glyph_id) * metrics.px_scale_factor
}
/// Height of one row of text in points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
#[inline(always)]
pub fn row_height(&self) -> f32 {
self.height_in_points
#[inline]
pub fn pair_kerning(
&self,
metrics: &ScaledMetrics,
last_glyph_id: ab_glyph::GlyphId,
glyph_id: ab_glyph::GlyphId,
) -> f32 {
self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point
}
#[inline(always)]
pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point
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 scale = font_size * self.tweak.scale * pixels_per_point;
let px_scale_factor = self.ab_glyph_font.px_scale_factor(scale);
let y_offset_in_points = ((font_size * self.tweak.scale * self.tweak.y_offset_factor)
+ self.tweak.y_offset)
.round_ui();
ScaledMetrics {
pixels_per_point,
px_scale_factor,
y_offset_in_points,
ascent,
row_height: ascent - descent + line_gap,
}
}
/// This is the distance from the top to the baseline.
///
/// Unit: points.
#[inline(always)]
pub fn ascent(&self) -> f32 {
self.ascent
}
pub fn allocate_glyph(
&mut self,
atlas: &mut TextureAtlas,
metrics: &ScaledMetrics,
glyph_info: GlyphInfo,
chr: char,
h_pos: f32,
) -> (GlyphAllocation, i32) {
let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor;
fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo {
assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0");
use ab_glyph::{Font as _, ScaleFont as _};
let Some(glyph_id) = glyph_info.id else {
// Invisible.
return (GlyphAllocation::default(), h_pos as i32);
};
let glyph = glyph_id.with_scale_and_position(
self.scale_in_pixels as f32,
ab_glyph::Point { x: 0.0, y: 0.0 },
);
// 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 {
SubpixelBin::new(h_pos)
};
let uv_rect = self.ab_glyph_font.outline_glyph(glyph).map(|glyph| {
let bb = glyph.px_bounds();
let entry = match self
.glyph_alloc_cache
.entry(GlyphCacheKey::new(glyph_id, metrics, bin))
{
std::collections::hash_map::Entry::Occupied(glyph_alloc) => {
let mut glyph_alloc = *glyph_alloc.get();
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);
}
std::collections::hash_map::Entry::Vacant(entry) => entry,
};
debug_assert!(glyph_id.0 != 0, "Can't allocate glyph for id 0");
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 atlas = &mut self.atlas.lock();
let text_alpha_from_coverage = atlas.text_alpha_from_coverage;
let (glyph_pos, image) = atlas.allocate((glyph_width, glyph_height));
glyph.draw(|x, y, v| {
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;
@@ -292,11 +412,11 @@ impl FontImpl {
};
let offset_in_pixels = vec2(bb.min.x, bb.min.y);
let offset =
offset_in_pixels / self.pixels_per_point + self.y_offset_in_points * Vec2::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) / self.pixels_per_point,
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,
@@ -307,79 +427,25 @@ impl FontImpl {
});
let uv_rect = uv_rect.unwrap_or_default();
let advance_width_in_points = self
.ab_glyph_font
.as_scaled(self.scale_in_pixels as f32)
.h_advance(glyph_id)
/ self.pixels_per_point;
GlyphInfo {
let allocation = GlyphAllocation {
id: glyph_id,
advance_width: advance_width_in_points,
advance_width_px,
uv_rect,
}
};
entry.insert(allocation);
(allocation, h_pos_round)
}
}
type FontIndex = usize;
// TODO(emilk): rename?
/// Wrapper over multiple [`FontImpl`] (e.g. a primary + fallbacks for emojis)
pub struct Font {
fonts: Vec<Arc<FontImpl>>,
/// Lazily calculated.
characters: Option<BTreeMap<char, Vec<String>>>,
replacement_glyph: (FontIndex, GlyphInfo),
pixels_per_point: f32,
row_height: f32,
glyph_info_cache: ahash::HashMap<char, (FontIndex, GlyphInfo)>,
pub struct Font<'a> {
pub(super) fonts_by_id: &'a mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
pub(super) cached_family: &'a mut CachedFamily,
pub(super) atlas: &'a mut TextureAtlas,
}
impl Font {
pub fn new(fonts: Vec<Arc<FontImpl>>) -> Self {
if fonts.is_empty() {
return Self {
fonts,
characters: None,
replacement_glyph: Default::default(),
pixels_per_point: 1.0,
row_height: 0.0,
glyph_info_cache: Default::default(),
};
}
let pixels_per_point = fonts[0].pixels_per_point();
let row_height = fonts[0].row_height();
let mut slf = Self {
fonts,
characters: None,
replacement_glyph: Default::default(),
pixels_per_point,
row_height,
glyph_info_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)
.or_else(|| slf.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR))
.unwrap_or_else(|| {
#[cfg(feature = "log")]
log::warn!(
"Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
);
(0, GlyphInfo::default())
});
slf.replacement_glyph = replacement_glyph;
slf
}
impl Font<'_> {
pub fn preload_characters(&mut self, s: &str) {
for c in s.chars() {
self.glyph_info(c);
@@ -399,9 +465,10 @@ impl Font {
/// All supported characters, and in which font they are available in.
pub fn characters(&mut self) -> &BTreeMap<char, Vec<String>> {
self.characters.get_or_insert_with(|| {
self.cached_family.characters.get_or_insert_with(|| {
let mut characters: BTreeMap<char, Vec<String>> = Default::default();
for font in &self.fonts {
for font_id in &self.cached_family.fonts {
let font = self.fonts_by_id.get(font_id).expect("Nonexistent font ID");
for chr in font.characters() {
characters.entry(chr).or_default().push(font.name.clone());
}
@@ -410,34 +477,29 @@ impl Font {
})
}
#[inline(always)]
pub fn round_to_pixel(&self, point: f32) -> f32 {
(point * self.pixels_per_point).round() / self.pixels_per_point
}
/// Height of one row of text. In points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
#[inline(always)]
pub fn row_height(&self) -> f32 {
self.row_height
}
pub fn uv_rect(&self, c: char) -> UvRect {
self.glyph_info_cache
.get(&c)
.map(|gi| gi.1.uv_rect)
pub fn scaled_metrics(&self, pixels_per_point: f32, font_size: f32) -> ScaledMetrics {
self.cached_family
.fonts
.first()
.and_then(|key| self.fonts_by_id.get(key))
.map(|font_impl| font_impl.scaled_metrics(pixels_per_point, font_size))
.unwrap_or_default()
}
/// Width of this character in points.
pub fn glyph_width(&mut self, c: char) -> f32 {
self.glyph_info(c).1.advance_width
pub fn glyph_width(&mut self, c: char, font_size: f32) -> f32 {
let (key, glyph_info) = self.glyph_info(c);
let font = &self
.fonts_by_id
.get(&key)
.expect("Nonexistent font ID")
.ab_glyph_font;
glyph_info.advance_width_unscaled.0 * 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.replacement_glyph // TODO(emilk): this is a false negative if the user asks about the replacement character itself 🤦‍♂️
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 🤦‍♂️
}
/// Can we display all the glyphs in this text?
@@ -446,44 +508,46 @@ impl Font {
}
/// `\n` will (intentionally) show up as the replacement character.
fn glyph_info(&mut self, c: char) -> (FontIndex, GlyphInfo) {
if let Some(font_index_glyph_info) = self.glyph_info_cache.get(&c) {
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;
}
let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c);
let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph);
self.glyph_info_cache.insert(c, font_index_glyph_info);
let font_index_glyph_info = 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
}
}
#[inline]
pub(crate) fn font_impl_and_glyph_info(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) {
if self.fonts.is_empty() {
return (None, self.replacement_glyph.1);
}
let (font_index, glyph_info) = self.glyph_info(c);
let font_impl = &self.fonts[font_index];
(Some(font_impl), glyph_info)
}
/// Metrics for a font at a specific screen-space scale.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct ScaledMetrics {
/// The DPI part of the screen-space scale.
pub pixels_per_point: f32,
pub(crate) fn ascent(&self) -> f32 {
if let Some(first) = self.fonts.first() {
first.ascent()
} else {
self.row_height
}
}
/// Scale factor, relative to the font's units per em (so, probably much less than 1).
///
/// Translates "unscaled" units to physical (screen) pixels.
pub px_scale_factor: f32,
fn glyph_info_no_cache_or_fallback(&mut self, c: char) -> Option<(FontIndex, GlyphInfo)> {
for (font_index, font_impl) in self.fonts.iter().enumerate() {
if let Some(glyph_info) = font_impl.glyph_info(c) {
self.glyph_info_cache.insert(c, (font_index, glyph_info));
return Some((font_index, glyph_info));
}
}
None
}
/// Vertical offset, in UI points.
pub y_offset_in_points: f32,
/// This is the distance from the top to the baseline.
///
/// Unit: points.
pub ascent: f32,
/// Height of one row of text in points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub row_height: f32,
}
/// Code points that will always be invisible (zero width).
@@ -532,3 +596,28 @@ fn invisible_char(c: char) -> bool {
| '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
)
}
#[inline]
pub(super) fn is_cjk_ideograph(c: char) -> bool {
('\u{4E00}' <= c && c <= '\u{9FFF}')
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
}
#[inline]
pub(super) fn is_kana(c: char) -> bool {
('\u{3040}' <= c && c <= '\u{309F}') // Hiragana block
|| ('\u{30A0}' <= c && c <= '\u{30FF}') // Katakana block
}
#[inline]
pub(super) fn is_cjk(c: char) -> bool {
// TODO(bigfarts): Add support for Korean Hangul.
is_cjk_ideograph(c) || is_kana(c)
}
#[inline]
pub(super) fn is_cjk_break_allowed(c: char) -> bool {
// See: https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages#Characters_not_permitted_on_the_start_of_a_line.
!")]}〕〉》」』】〙〗〟'\"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。.".contains(c)
}

View File

@@ -1,11 +1,16 @@
use std::{collections::BTreeMap, sync::Arc};
use std::{
collections::BTreeMap,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use crate::{
AlphaFromCoverage, TextureAtlas,
mutex::{Mutex, MutexGuard},
text::{
Galley, LayoutJob, LayoutSection,
font::{Font, FontImpl},
font::{Font, FontImpl, GlyphInfo},
},
};
use emath::{NumExt as _, OrderedFloat};
@@ -179,13 +184,6 @@ pub struct FontTweak {
///
/// Example value: `2.0`.
pub y_offset: f32,
/// When using this font's metrics to layout a row,
/// shift the entire row downwards by this fraction of the font size (in points).
///
/// A positive value shifts the text downwards.
/// A negative value shifts it upwards.
pub baseline_offset_factor: f32,
}
impl Default for FontTweak {
@@ -194,7 +192,6 @@ impl Default for FontTweak {
scale: 1.0,
y_offset_factor: 0.0,
y_offset: 0.0,
baseline_offset_factor: 0.0,
}
}
}
@@ -407,6 +404,92 @@ impl FontDefinitions {
}
}
/// Unique ID for looking up a single font face/file.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct FontFaceKey(u64);
impl FontFaceKey {
pub const INVALID: Self = Self(0);
fn new() -> Self {
static KEY_COUNTER: AtomicU64 = AtomicU64::new(1);
Self(crate::util::hash(
KEY_COUNTER.fetch_add(1, Ordering::Relaxed),
))
}
}
// Safe, because we hash the value in the constructor.
impl nohash_hasher::IsEnabled for FontFaceKey {}
/// Cached data for working with a font family (e.g. doing character lookups).
#[derive(Debug)]
pub(super) struct CachedFamily {
pub fonts: Vec<FontFaceKey>,
/// Lazily calculated.
pub characters: Option<BTreeMap<char, Vec<String>>>,
pub replacement_glyph: (FontFaceKey, GlyphInfo),
pub glyph_info_cache: ahash::HashMap<char, (FontFaceKey, GlyphInfo)>,
}
impl CachedFamily {
fn new(
fonts: Vec<FontFaceKey>,
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
) -> Self {
if fonts.is_empty() {
return Self {
fonts,
characters: None,
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
glyph_info_cache: Default::default(),
};
}
let mut slf = Self {
fonts,
characters: None,
replacement_glyph: (FontFaceKey::INVALID, GlyphInfo::INVISIBLE),
glyph_info_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))
.unwrap_or_else(|| {
#[cfg(feature = "log")]
log::warn!(
"Failed to find replacement characters {PRIMARY_REPLACEMENT_CHAR:?} or {FALLBACK_REPLACEMENT_CHAR:?}. Will use empty glyph."
);
(FontFaceKey::INVALID, GlyphInfo::INVISIBLE)
});
slf.replacement_glyph = replacement_glyph;
slf
}
pub(crate) fn glyph_info_no_cache_or_fallback(
&mut self,
c: char,
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
) -> 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) {
self.glyph_info_cache.insert(c, (*font_key, glyph_info));
return Some((*font_key, glyph_info));
}
}
None
}
}
// ----------------------------------------------------------------------------
/// The collection of fonts used by `epaint`.
@@ -418,31 +501,25 @@ impl FontDefinitions {
/// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`.
///
/// You need to call [`Self::begin_pass`] and [`Self::font_image_delta`] once every frame.
#[derive(Clone)]
pub struct Fonts(Arc<Mutex<FontsAndCache>>);
pub struct Fonts {
pub fonts: FontsImpl,
galley_cache: GalleyCache,
}
impl Fonts {
/// Create a new [`Fonts`] for text layout.
/// This call is expensive, so only create one [`Fonts`] and then reuse it.
///
/// * `pixels_per_point`: how many physical pixels per logical "point".
/// * `max_texture_side`: largest supported texture size (one side).
pub fn new(
pixels_per_point: f32,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
definitions: FontDefinitions,
) -> Self {
let fonts_and_cache = FontsAndCache {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
Self {
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
galley_cache: Default::default(),
};
Self(Arc::new(Mutex::new(fonts_and_cache)))
}
}
/// Call at the start of each frame with the latest known
@@ -453,114 +530,156 @@ impl Fonts {
/// This function will react to changes in `pixels_per_point`, `max_texture_side`, and `text_alpha_from_coverage`,
/// as well as notice when the font atlas is getting full, and handle that.
pub fn begin_pass(
&self,
pixels_per_point: f32,
&mut self,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
) {
let mut fonts_and_cache = self.0.lock();
let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point;
let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side;
let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side;
let text_alpha_from_coverage_changed =
fonts_and_cache.fonts.atlas.lock().text_alpha_from_coverage != text_alpha_from_coverage;
let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8;
let needs_recreate = pixels_per_point_changed
|| max_texture_side_changed
|| text_alpha_from_coverage_changed
|| font_atlas_almost_full;
self.fonts.atlas.text_alpha_from_coverage != text_alpha_from_coverage;
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;
if needs_recreate {
let definitions = fonts_and_cache.fonts.definitions.clone();
let definitions = self.fonts.definitions.clone();
*fonts_and_cache = FontsAndCache {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
*self = Self {
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
galley_cache: Default::default(),
};
}
fonts_and_cache.galley_cache.flush_cache();
self.galley_cache.flush_cache();
}
/// Call at the end of each frame (before painting) to get the change to the font texture since last call.
pub fn font_image_delta(&self) -> Option<crate::ImageDelta> {
self.lock().fonts.atlas.lock().take_delta()
}
/// Access the underlying [`FontsAndCache`].
#[doc(hidden)]
#[inline]
pub fn lock(&self) -> MutexGuard<'_, FontsAndCache> {
self.0.lock()
}
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.lock().fonts.pixels_per_point
pub fn font_image_delta(&mut self) -> Option<crate::ImageDelta> {
self.fonts.atlas.take_delta()
}
#[inline]
pub fn max_texture_side(&self) -> usize {
self.lock().fonts.max_texture_side
self.fonts.max_texture_side
}
#[inline]
pub fn definitions(&self) -> &FontDefinitions {
&self.fonts.definitions
}
/// The font atlas.
/// Pass this to [`crate::Tessellator`].
pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
self.lock().fonts.atlas.clone()
pub fn texture_atlas(&self) -> &TextureAtlas {
&self.fonts.atlas
}
/// The full font atlas image.
#[inline]
pub fn image(&self) -> crate::ColorImage {
self.lock().fonts.atlas.lock().image().clone()
self.fonts.atlas.image().clone()
}
/// Current size of the font image.
/// Pass this to [`crate::Tessellator`].
pub fn font_image_size(&self) -> [usize; 2] {
self.lock().fonts.atlas.lock().size()
}
/// Width of this character in points.
#[inline]
pub fn glyph_width(&self, font_id: &FontId, c: char) -> f32 {
self.lock().fonts.glyph_width(font_id, c)
self.fonts.atlas.size()
}
/// Can we display this glyph?
#[inline]
pub fn has_glyph(&self, font_id: &FontId, c: char) -> bool {
self.lock().fonts.has_glyph(font_id, c)
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
self.fonts.font(&font_id.family).has_glyph(c)
}
/// Can we display all the glyphs in this text?
pub fn has_glyphs(&self, font_id: &FontId, s: &str) -> bool {
self.lock().fonts.has_glyphs(font_id, s)
pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
self.fonts.font(&font_id.family).has_glyphs(s)
}
pub fn num_galleys_in_cache(&self) -> usize {
self.galley_cache.num_galleys_in_cache()
}
/// How full is the font atlas?
///
/// This increases as new fonts and/or glyphs are used,
/// but can also decrease in a call to [`Self::begin_pass`].
pub fn font_atlas_fill_ratio(&self) -> f32 {
self.fonts.atlas.fill_ratio()
}
/// Returns a [`FontsView`] with the given `pixels_per_point` that can be used to do text layout.
pub fn with_pixels_per_point(&mut self, pixels_per_point: f32) -> FontsView<'_> {
FontsView {
fonts: &mut self.fonts,
galley_cache: &mut self.galley_cache,
pixels_per_point,
}
}
}
// ----------------------------------------------------------------------------
/// The context's collection of fonts, with this context's `pixels_per_point`. This is what you use to do text layout.
pub struct FontsView<'a> {
pub fonts: &'a mut FontsImpl,
galley_cache: &'a mut GalleyCache,
pixels_per_point: f32,
}
impl FontsView<'_> {
#[inline]
pub fn max_texture_side(&self) -> usize {
self.fonts.max_texture_side
}
#[inline]
pub fn definitions(&self) -> &FontDefinitions {
&self.fonts.definitions
}
/// The full font atlas image.
#[inline]
pub fn image(&self) -> crate::ColorImage {
self.fonts.atlas.image().clone()
}
/// Current size of the font image.
/// Pass this to [`crate::Tessellator`].
pub fn font_image_size(&self) -> [usize; 2] {
self.fonts.atlas.size()
}
/// Width of this character in points.
pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
self.fonts
.font(&font_id.family)
.glyph_width(c, font_id.size)
}
/// Can we display this glyph?
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
self.fonts.font(&font_id.family).has_glyph(c)
}
/// Can we display all the glyphs in this text?
pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
self.fonts.font(&font_id.family).has_glyphs(s)
}
/// Height of one row of text in points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
#[inline]
pub fn row_height(&self, font_id: &FontId) -> f32 {
self.lock().fonts.row_height(font_id)
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
self.fonts
.font(&font_id.family)
.scaled_metrics(self.pixels_per_point, font_id.size)
.row_height
}
/// List of all known font families.
pub fn families(&self) -> Vec<FontFamily> {
self.lock()
.fonts
.definitions
.families
.keys()
.cloned()
.collect()
self.fonts.definitions.families.keys().cloned().collect()
}
/// Layout some text.
@@ -571,27 +690,33 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
#[inline]
pub fn layout_job(&self, job: LayoutJob) -> Arc<Galley> {
self.lock().layout_job(job)
pub fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs.
self.galley_cache.layout(
self.fonts,
self.pixels_per_point,
job,
allow_split_paragraphs,
)
}
pub fn num_galleys_in_cache(&self) -> usize {
self.lock().galley_cache.num_galleys_in_cache()
self.galley_cache.num_galleys_in_cache()
}
/// How full is the font atlas?
///
/// This increases as new fonts and/or glyphs are used,
/// but can also decrease in a call to [`Self::begin_pass`].
/// but can also decrease in a call to [`Fonts::begin_pass`].
pub fn font_atlas_fill_ratio(&self) -> f32 {
self.lock().fonts.atlas.lock().fill_ratio()
self.fonts.atlas.fill_ratio()
}
/// Will wrap text at the given width and line break at `\n`.
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout(
&self,
&mut self,
text: String,
font_id: FontId,
color: crate::Color32,
@@ -605,7 +730,7 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout_no_wrap(
&self,
&mut self,
text: String,
font_id: FontId,
color: crate::Color32,
@@ -618,7 +743,7 @@ impl Fonts {
///
/// The implementation uses memoization so repeated calls are cheap.
pub fn layout_delayed_color(
&self,
&mut self,
text: String,
font_id: FontId,
wrap_width: f32,
@@ -629,118 +754,75 @@ impl Fonts {
// ----------------------------------------------------------------------------
pub struct FontsAndCache {
pub fonts: FontsImpl,
galley_cache: GalleyCache,
}
impl FontsAndCache {
fn layout_job(&mut self, job: LayoutJob) -> Arc<Galley> {
let allow_split_paragraphs = true; // Optimization for editing text with many paragraphs.
self.galley_cache
.layout(&mut self.fonts, job, allow_split_paragraphs)
}
}
// ----------------------------------------------------------------------------
/// The collection of fonts used by `epaint`.
///
/// Required in order to paint text.
pub struct FontsImpl {
pixels_per_point: f32,
max_texture_side: usize,
definitions: FontDefinitions,
atlas: Arc<Mutex<TextureAtlas>>,
font_impl_cache: FontImplCache,
sized_family: ahash::HashMap<(OrderedFloat<f32>, FontFamily), Font>,
atlas: TextureAtlas,
fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl>,
fonts_by_name: ahash::HashMap<String, FontFaceKey>,
family_cache: ahash::HashMap<FontFamily, CachedFamily>,
}
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(
pixels_per_point: f32,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
definitions: FontDefinitions,
) -> Self {
assert!(
0.0 < pixels_per_point && pixels_per_point < 100.0,
"pixels_per_point out of range: {pixels_per_point}"
);
let texture_width = 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 = Arc::new(Mutex::new(atlas));
let font_impl_cache =
FontImplCache::new(atlas.clone(), pixels_per_point, &definitions.font_data);
let mut fonts_by_id: nohash_hasher::IntMap<FontFaceKey, FontImpl> = Default::default();
let mut font_impls: 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 key = FontFaceKey::new();
fonts_by_id.insert(key, font_impl);
font_impls.insert(name.clone(), key);
}
Self {
pixels_per_point,
max_texture_side,
definitions,
atlas,
font_impl_cache,
sized_family: Default::default(),
fonts_by_id,
fonts_by_name: font_impls,
family_cache: Default::default(),
}
}
#[inline(always)]
pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point
}
/// 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(|| {
let fonts = &self.definitions.families.get(family);
let fonts =
fonts.unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
#[inline]
pub fn definitions(&self) -> &FontDefinitions {
&self.definitions
}
let fonts: Vec<FontFaceKey> = fonts
.iter()
.map(|font_name| {
*self
.fonts_by_name
.get(font_name)
.unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
})
.collect();
/// Get the right font implementation from size and [`FontFamily`].
pub fn font(&mut self, font_id: &FontId) -> &mut Font {
let FontId { size, family } = font_id;
let mut size = *size;
size = size.at_least(0.1).at_most(2048.0);
self.sized_family
.entry((OrderedFloat(size), family.clone()))
.or_insert_with(|| {
let fonts = &self.definitions.families.get(family);
let fonts = fonts
.unwrap_or_else(|| panic!("FontFamily::{family:?} is not bound to any fonts"));
let fonts: Vec<Arc<FontImpl>> = fonts
.iter()
.map(|font_name| self.font_impl_cache.font_impl(size, font_name))
.collect();
Font::new(fonts)
})
}
/// Width of this character in points.
fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
self.font(font_id).glyph_width(c)
}
/// Can we display this glyph?
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
self.font(font_id).has_glyph(c)
}
/// Can we display all the glyphs in this text?
pub fn has_glyphs(&mut self, font_id: &FontId, s: &str) -> bool {
self.font(font_id).has_glyphs(s)
}
/// Height of one row of text in points.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
fn row_height(&mut self, font_id: &FontId) -> f32 {
self.font(font_id).row_height()
CachedFamily::new(fonts, &mut self.fonts_by_id)
});
Font {
fonts_by_id: &mut self.fonts_by_id,
cached_family,
atlas: &mut self.atlas,
}
}
}
@@ -770,6 +852,7 @@ impl GalleyCache {
&mut self,
fonts: &mut FontsImpl,
mut job: LayoutJob,
pixels_per_point: f32,
allow_split_paragraphs: bool,
) -> (u64, Arc<Galley>) {
if job.wrap.max_width.is_finite() {
@@ -797,7 +880,7 @@ impl GalleyCache {
job.wrap.max_width = job.wrap.max_width.round();
}
let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher?
let hash = crate::util::hash((&job, OrderedFloat(pixels_per_point))); // TODO(emilk): even faster hasher?
let galley = match self.cache.entry(hash) {
std::collections::hash_map::Entry::Occupied(entry) => {
@@ -825,14 +908,13 @@ impl GalleyCache {
let job = Arc::new(job);
if allow_split_paragraphs && should_cache_each_paragraph_individually(&job) {
let (child_galleys, child_hashes) =
self.layout_each_paragraph_individually(fonts, &job);
self.layout_each_paragraph_individually(fonts, &job, pixels_per_point);
debug_assert_eq!(
child_hashes.len(),
child_galleys.len(),
"Bug in `layout_each_paragraph_individuallly`"
);
let galley =
Arc::new(Galley::concat(job, &child_galleys, fonts.pixels_per_point));
let galley = Arc::new(Galley::concat(job, &child_galleys, pixels_per_point));
self.cache.insert(
hash,
@@ -844,7 +926,7 @@ impl GalleyCache {
);
galley
} else {
let galley = super::layout(fonts, job);
let galley = super::layout(fonts, pixels_per_point, job);
let galley = Arc::new(galley);
entry.insert(CachedGalley {
last_used: self.generation,
@@ -862,10 +944,12 @@ impl GalleyCache {
fn layout(
&mut self,
fonts: &mut FontsImpl,
pixels_per_point: f32,
job: LayoutJob,
allow_split_paragraphs: bool,
) -> Arc<Galley> {
self.layout_internal(fonts, job, allow_split_paragraphs).1
self.layout_internal(fonts, job, pixels_per_point, allow_split_paragraphs)
.1
}
/// Split on `\n` and lay out (and cache) each paragraph individually.
@@ -873,6 +957,7 @@ impl GalleyCache {
&mut self,
fonts: &mut FontsImpl,
job: &LayoutJob,
pixels_per_point: f32,
) -> (Vec<Arc<Galley>>, Vec<u64>) {
profiling::function_scope!();
@@ -952,7 +1037,8 @@ impl GalleyCache {
}
// TODO(emilk): we could lay out each paragraph in parallel to get a nice speedup on multicore machines.
let (hash, galley) = self.layout_internal(fonts, paragraph_job, false);
let (hash, galley) =
self.layout_internal(fonts, paragraph_job, pixels_per_point, false);
child_hashes.push(hash);
// This will prevent us from invalidating cache entries unnecessarily:
@@ -996,77 +1082,6 @@ fn should_cache_each_paragraph_individually(job: &LayoutJob) -> bool {
job.break_on_newline && job.wrap.max_rows == usize::MAX && job.text.contains('\n')
}
// ----------------------------------------------------------------------------
struct FontImplCache {
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
ab_glyph_fonts: BTreeMap<String, (FontTweak, ab_glyph::FontArc)>,
/// Map font pixel sizes and names to the cached [`FontImpl`].
cache: ahash::HashMap<(u32, String), Arc<FontImpl>>,
}
impl FontImplCache {
pub fn new(
atlas: Arc<Mutex<TextureAtlas>>,
pixels_per_point: f32,
font_data: &BTreeMap<String, Arc<FontData>>,
) -> Self {
let ab_glyph_fonts = font_data
.iter()
.map(|(name, font_data)| {
let tweak = font_data.tweak;
let ab_glyph = ab_glyph_font_from_font_data(name, font_data);
(name.clone(), (tweak, ab_glyph))
})
.collect();
Self {
atlas,
pixels_per_point,
ab_glyph_fonts,
cache: Default::default(),
}
}
pub fn font_impl(&mut self, scale_in_points: f32, font_name: &str) -> Arc<FontImpl> {
use ab_glyph::Font as _;
let (tweak, ab_glyph_font) = self
.ab_glyph_fonts
.get(font_name)
.unwrap_or_else(|| panic!("No font data found for {font_name:?}"))
.clone();
let scale_in_pixels = self.pixels_per_point * scale_in_points;
// Scale the font properly (see https://github.com/emilk/egui/issues/2068).
let units_per_em = ab_glyph_font.units_per_em().unwrap_or_else(|| {
panic!("The font unit size of {font_name:?} exceeds the expected range (16..=16384)")
});
let font_scaling = ab_glyph_font.height_unscaled() / units_per_em;
let scale_in_pixels = scale_in_pixels * font_scaling;
self.cache
.entry((
(scale_in_pixels * tweak.scale).round() as u32,
font_name.to_owned(),
))
.or_insert_with(|| {
Arc::new(FontImpl::new(
self.atlas.clone(),
self.pixels_per_point,
font_name.to_owned(),
ab_glyph_font,
scale_in_pixels,
tweak,
))
})
.clone()
}
}
#[cfg(feature = "default_fonts")]
#[cfg(test)]
mod tests {
@@ -1178,7 +1193,6 @@ mod tests {
for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
let max_texture_side = 4096;
let mut fonts = FontsImpl::new(
pixels_per_point,
max_texture_side,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1190,9 +1204,19 @@ mod tests {
job.halign = halign;
job.justify = justify;
let whole = GalleyCache::default().layout(&mut fonts, job.clone(), false);
let whole = GalleyCache::default().layout(
&mut fonts,
pixels_per_point,
job.clone(),
false,
);
let split = GalleyCache::default().layout(&mut fonts, job.clone(), true);
let split = GalleyCache::default().layout(
&mut fonts,
pixels_per_point,
job.clone(),
true,
);
for (i, row) in whole.rows.iter().enumerate() {
println!(
@@ -1231,7 +1255,6 @@ mod tests {
for pixels_per_point in pixels_per_point {
let mut fonts = FontsImpl::new(
pixels_per_point,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1244,12 +1267,13 @@ mod tests {
job.round_output_to_gui = round_output_to_gui;
let galley_wrapped = layout(&mut fonts, job.clone().into());
let galley_wrapped =
layout(&mut fonts, pixels_per_point, job.clone().into());
job.wrap = TextWrapping::no_max_width();
let text = job.text.clone();
let galley_unwrapped = layout(&mut fonts, job.into());
let galley_unwrapped = layout(&mut fonts, pixels_per_point, job.into());
let intrinsic_size = galley_wrapped.intrinsic_size();
let unwrapped_size = galley_unwrapped.size();

View File

@@ -12,7 +12,7 @@ pub const TAB_SIZE: usize = 4;
pub use {
fonts::{
FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts,
FontsImpl, InsertFontFamily,
FontsImpl, FontsView, InsertFontFamily,
},
text_layout::*,
text_layout_types::*,

View File

@@ -2,7 +2,14 @@ use std::sync::Arc;
use emath::{Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2, pos2, vec2};
use crate::{Color32, Mesh, Stroke, Vertex, stroke::PathStroke, text::font::Font};
use crate::{
Color32, Mesh, Stroke, Vertex,
stroke::PathStroke,
text::{
font::{ScaledMetrics, is_cjk, is_cjk_break_allowed},
fonts::FontFaceKey,
},
};
use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals};
@@ -41,8 +48,8 @@ impl PointScale {
/// Temporary storage before line-wrapping.
#[derive(Clone)]
struct Paragraph {
/// Start of the next glyph to be added.
pub cursor_x: f32,
/// Start of the next glyph to be added. In screen-space / physical pixels.
pub cursor_x_px: f32,
/// This is included in case there are no glyphs
pub section_index_at_start: u32,
@@ -56,7 +63,7 @@ struct Paragraph {
impl Paragraph {
pub fn from_section_index(section_index_at_start: u32) -> Self {
Self {
cursor_x: 0.0,
cursor_x_px: 0.0,
section_index_at_start,
glyphs: vec![],
empty_paragraph_height: 0.0,
@@ -66,9 +73,9 @@ impl Paragraph {
/// Layout text into a [`Galley`].
///
/// In most cases you should use [`crate::Fonts::layout_job`] instead
/// In most cases you should use [`crate::FontsView::layout_job`] instead
/// since that memoizes the input, making subsequent layouting of the same text much faster.
pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>) -> Galley {
profiling::function_scope!();
if job.wrap.max_rows == 0 {
@@ -80,7 +87,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
mesh_bounds: Rect::NOTHING,
num_vertices: 0,
num_indices: 0,
pixels_per_point: fonts.pixels_per_point(),
pixels_per_point,
elided: true,
intrinsic_size: Vec2::ZERO,
};
@@ -90,10 +97,17 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
let mut paragraphs = vec![Paragraph::from_section_index(0)];
for (section_index, section) in job.sections.iter().enumerate() {
layout_section(fonts, &job, section_index as u32, section, &mut paragraphs);
layout_section(
fonts,
pixels_per_point,
&job,
section_index as u32,
section,
&mut paragraphs,
);
}
let point_scale = PointScale::new(fonts.pixels_per_point());
let point_scale = PointScale::new(pixels_per_point);
let intrinsic_size = calculate_intrinsic_size(point_scale, &job, &paragraphs);
@@ -102,7 +116,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
if elided {
if let Some(last_placed) = rows.last_mut() {
let last_row = Arc::make_mut(&mut last_placed.row);
replace_last_glyph_with_overflow_character(fonts, &job, last_row);
replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row);
if let Some(last) = last_row.glyphs.last() {
last_row.size.x = last.max_x();
}
@@ -133,6 +147,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
// Ignores the Y coordinate.
fn layout_section(
fonts: &mut FontsImpl,
pixels_per_point: f32,
job: &LayoutJob,
section_index: u32,
section: &LayoutSection,
@@ -143,11 +158,13 @@ fn layout_section(
byte_range,
format,
} = section;
let font = fonts.font(&format.font_id);
let mut font = fonts.font(&format.font_id.family);
let font_size = format.font_id.size;
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let line_height = section
.format
.line_height
.unwrap_or_else(|| font.row_height());
.unwrap_or(font_metrics.row_height);
let extra_letter_spacing = section.format.extra_letter_spacing;
let mut paragraph = out_paragraphs.last_mut().unwrap();
@@ -155,40 +172,70 @@ fn layout_section(
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
}
paragraph.cursor_x += leading_space;
paragraph.cursor_x_px += leading_space * pixels_per_point;
let mut last_glyph_id = None;
// Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
let mut current_font = FontFaceKey::INVALID;
let mut current_font_impl_metrics = ScaledMetrics::default();
for chr in job.text[byte_range.clone()].chars() {
if job.break_on_newline && chr == '\n' {
out_paragraphs.push(Paragraph::from_section_index(section_index));
paragraph = out_paragraphs.last_mut().unwrap();
paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
} else {
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(chr);
if let Some(font_impl) = font_impl {
if let Some(last_glyph_id) = last_glyph_id {
paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id);
paragraph.cursor_x += extra_letter_spacing;
}
let (font_id, glyph_info) = font.glyph_info(chr);
let mut font_impl = font.fonts_by_id.get_mut(&font_id);
if current_font != font_id {
current_font = font_id;
current_font_impl_metrics = font_impl
.as_ref()
.map(|font_impl| font_impl.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)
{
paragraph.cursor_x_px += font_impl.pair_kerning_pixels(
&current_font_impl_metrics,
last_glyph_id,
glyph_id,
);
// Only apply extra_letter_spacing to glyphs after the first one:
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(
font.atlas,
&current_font_impl_metrics,
glyph_info,
chr,
paragraph.cursor_x_px,
)
} else {
Default::default()
};
paragraph.glyphs.push(Glyph {
chr,
pos: pos2(paragraph.cursor_x, f32::NAN),
advance_width: glyph_info.advance_width,
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: font_impl.map_or(0.0, |f| f.row_height()),
font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()),
font_height: font.row_height(),
font_ascent: font.ascent(),
uv_rect: glyph_info.uv_rect,
font_impl_height: current_font_impl_metrics.row_height,
font_impl_ascent: current_font_impl_metrics.ascent,
font_height: font_metrics.row_height,
font_ascent: font_metrics.ascent,
uv_rect: glyph_alloc.uv_rect,
section_index,
});
paragraph.cursor_x += glyph_info.advance_width;
paragraph.cursor_x = font.round_to_pixel(paragraph.cursor_x);
last_glyph_id = Some(glyph_info.id);
paragraph.cursor_x_px += glyph_alloc.advance_width_px;
last_glyph_id = Some(glyph_alloc.id);
}
}
}
@@ -398,149 +445,105 @@ fn line_break(
/// Called before we have any Y coordinates.
fn replace_last_glyph_with_overflow_character(
fonts: &mut FontsImpl,
pixels_per_point: f32,
job: &LayoutJob,
row: &mut Row,
) {
fn row_width(row: &Row) -> f32 {
if let (Some(first), Some(last)) = (row.glyphs.first(), row.glyphs.last()) {
last.max_x() - first.pos.x
} else {
0.0
}
}
fn row_height(section: &LayoutSection, font: &Font) -> f32 {
section
.format
.line_height
.unwrap_or_else(|| font.row_height())
}
let Some(overflow_character) = job.wrap.overflow_character else {
return;
};
// We always try to just append the character first:
if let Some(last_glyph) = row.glyphs.last() {
let section_index = last_glyph.section_index;
let section = &job.sections[section_index as usize];
let font = fonts.font(&section.format.font_id);
let line_height = row_height(section, font);
let (_, last_glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
let mut x = last_glyph.pos.x + last_glyph.advance_width;
let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character);
{
// Kerning:
x += section.format.extra_letter_spacing;
if let Some(font_impl) = font_impl {
x += font_impl.pair_kerning(last_glyph_info.id, replacement_glyph_info.id);
}
}
row.glyphs.push(Glyph {
chr: overflow_character,
pos: pos2(x, f32::NAN),
advance_width: replacement_glyph_info.advance_width,
line_height,
font_impl_height: font_impl.map_or(0.0, |f| f.row_height()),
font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()),
font_height: font.row_height(),
font_ascent: font.ascent(),
uv_rect: replacement_glyph_info.uv_rect,
section_index,
});
} else {
let section_index = row.section_index_at_start;
let section = &job.sections[section_index as usize];
let font = fonts.font(&section.format.font_id);
let line_height = row_height(section, font);
let x = 0.0; // TODO(emilk): heed paragraph leading_space 😬
let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character);
row.glyphs.push(Glyph {
chr: overflow_character,
pos: pos2(x, f32::NAN),
advance_width: replacement_glyph_info.advance_width,
line_height,
font_impl_height: font_impl.map_or(0.0, |f| f.row_height()),
font_impl_ascent: font_impl.map_or(0.0, |f| f.ascent()),
font_height: font.row_height(),
font_ascent: font.ascent(),
uv_rect: replacement_glyph_info.uv_rect,
section_index,
});
}
if row_width(row) <= job.effective_wrap_width() || row.glyphs.len() == 1 {
return; // we are done
}
// We didn't fit it. Remove it again…
row.glyphs.pop();
// …then go into a loop where we replace the last character with the overflow character
// until we fit within the max_width:
let mut section_index = row
.glyphs
.last()
.map(|g| g.section_index)
.unwrap_or(row.section_index_at_start);
loop {
let (prev_glyph, last_glyph) = match row.glyphs.as_mut_slice() {
[.., prev, last] => (Some(prev), last),
[.., last] => (None, last),
_ => {
unreachable!("We've already explicitly handled the empty row");
}
let section = &job.sections[section_index as usize];
let extra_letter_spacing = section.format.extra_letter_spacing;
let mut font = fonts.font(&section.format.font_id.family);
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
.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
.as_mut()
.map(|font_impl| {
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_impl.pair_kerning(&font_impl_metrics, prev_glyph_id, overflow_glyph_id)
} else {
0.0
}
})
.unwrap_or_default();
prev_glyph.max_x() + extra_letter_spacing + pair_kerning
} else {
0.0 // TODO(emilk): heed paragraph leading_space 😬
};
let section = &job.sections[last_glyph.section_index as usize];
let extra_letter_spacing = section.format.extra_letter_spacing;
let font = fonts.font(&section.format.font_id);
let replacement_glyph_width = font_impl
.as_mut()
.and_then(|f| f.glyph_info(overflow_character))
.map(|i| i.advance_width_unscaled.0 * font_impl_metrics.px_scale_factor)
.unwrap_or_default();
if let Some(prev_glyph) = prev_glyph {
let prev_glyph_id = font.font_impl_and_glyph_info(prev_glyph.chr).1.id;
// Check if we're within width budget:
if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
|| row.glyphs.is_empty()
{
// we are done
// Undo kerning with previous glyph:
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
last_glyph.pos.x -= extra_letter_spacing;
if let Some(font_impl) = font_impl {
last_glyph.pos.x -= font_impl.pair_kerning(prev_glyph_id, glyph_info.id);
}
let (replacement_glyph_alloc, physical_x) = font_impl
.as_mut()
.map(|f| {
f.allocate_glyph(
font.atlas,
&font_impl_metrics,
glyph_info,
overflow_character,
overflow_glyph_x * pixels_per_point,
)
})
.unwrap_or_default();
// Replace the glyph:
last_glyph.chr = overflow_character;
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
last_glyph.advance_width = glyph_info.advance_width;
last_glyph.font_impl_ascent = font_impl.map_or(0.0, |f| f.ascent());
last_glyph.font_impl_height = font_impl.map_or(0.0, |f| f.row_height());
last_glyph.uv_rect = glyph_info.uv_rect;
let font_metrics = font.scaled_metrics(pixels_per_point, font_size);
let line_height = section
.format
.line_height
.unwrap_or(font_metrics.row_height);
// Reapply kerning:
last_glyph.pos.x += extra_letter_spacing;
if let Some(font_impl) = font_impl {
last_glyph.pos.x += font_impl.pair_kerning(prev_glyph_id, glyph_info.id);
}
// Check if we're within width budget:
if row_width(row) <= job.effective_wrap_width() || row.glyphs.len() == 1 {
return; // We are done
}
// We didn't fit - pop the last glyph and try again.
row.glyphs.pop();
} else {
// Just replace and be done with it.
last_glyph.chr = overflow_character;
let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr);
last_glyph.advance_width = glyph_info.advance_width;
last_glyph.font_impl_ascent = font_impl.map_or(0.0, |f| f.ascent());
last_glyph.font_impl_height = font_impl.map_or(0.0, |f| f.row_height());
last_glyph.uv_rect = glyph_info.uv_rect;
row.glyphs.push(Glyph {
chr: 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_height: font_metrics.row_height,
font_ascent: font_metrics.ascent,
uv_rect: replacement_glyph_alloc.uv_rect,
section_index,
});
return;
}
// We didn't fit - pop the last glyph and try again.
if let Some(last_glyph) = row.glyphs.pop() {
section_index = last_glyph.section_index;
} else {
section_index = row.section_index_at_start;
}
}
}
@@ -1043,31 +1046,6 @@ impl RowBreakCandidates {
}
}
#[inline]
fn is_cjk_ideograph(c: char) -> bool {
('\u{4E00}' <= c && c <= '\u{9FFF}')
|| ('\u{3400}' <= c && c <= '\u{4DBF}')
|| ('\u{2B740}' <= c && c <= '\u{2B81F}')
}
#[inline]
fn is_kana(c: char) -> bool {
('\u{3040}' <= c && c <= '\u{309F}') // Hiragana block
|| ('\u{30A0}' <= c && c <= '\u{30FF}') // Katakana block
}
#[inline]
fn is_cjk(c: char) -> bool {
// TODO(bigfarts): Add support for Korean Hangul.
is_cjk_ideograph(c) || is_kana(c)
}
#[inline]
fn is_cjk_break_allowed(c: char) -> bool {
// See: https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages#Characters_not_permitted_on_the_start_of_a_line.
!")]}〕〉》」』】〙〗〟'\"⦆»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、:;,。.".contains(c)
}
// ----------------------------------------------------------------------------
#[cfg(test)]
@@ -1078,15 +1056,15 @@ mod tests {
#[test]
fn test_zero_max_width() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::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, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert_eq!(galley.rows.len(), 1);
}
@@ -1094,8 +1072,9 @@ mod tests {
fn test_truncate_with_newline() {
// No matter where we wrap, we should be appending the newline character.
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1114,7 +1093,7 @@ mod tests {
layout_job.wrap.max_rows = 1;
layout_job.wrap.break_anywhere = break_anywhere;
let galley = layout(&mut fonts, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert!(galley.elided);
assert_eq!(galley.rows.len(), 1);
@@ -1133,7 +1112,7 @@ mod tests {
layout_job.wrap.max_rows = 1;
layout_job.wrap.break_anywhere = false;
let galley = layout(&mut fonts, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert!(galley.elided);
assert_eq!(galley.rows.len(), 1);
@@ -1144,8 +1123,8 @@ mod tests {
#[test]
fn test_cjk() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1155,7 +1134,7 @@ mod tests {
TextFormat::default(),
);
layout_job.wrap.max_width = 90.0;
let galley = layout(&mut fonts, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
vec!["日本語と", "Englishの混在", "した文章"]
@@ -1164,8 +1143,8 @@ mod tests {
#[test]
fn test_pre_cjk() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1175,7 +1154,7 @@ mod tests {
TextFormat::default(),
);
layout_job.wrap.max_width = 110.0;
let galley = layout(&mut fonts, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
vec!["日本語とEnglish", "の混在した文章"]
@@ -1184,8 +1163,8 @@ mod tests {
#[test]
fn test_truncate_width() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1195,7 +1174,7 @@ mod tests {
layout_job.wrap.max_width = f32::INFINITY;
layout_job.wrap.max_rows = 1;
layout_job.round_output_to_gui = false;
let galley = layout(&mut fonts, layout_job.into());
let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
assert!(galley.elided);
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
@@ -1208,19 +1187,22 @@ mod tests {
#[test]
fn test_empty_row() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let font_id = FontId::default();
let font_height = fonts.font(&font_id).row_height();
let font_height = fonts
.font(&font_id.family)
.scaled_metrics(pixels_per_point, font_id.size)
.row_height;
let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
let galley = layout(&mut fonts, job.into());
let galley = layout(&mut fonts, pixels_per_point, job.into());
assert_eq!(galley.rows.len(), 1, "Expected one row");
assert_eq!(
@@ -1242,19 +1224,22 @@ mod tests {
#[test]
fn test_end_with_newline() {
let pixels_per_point = 1.0;
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);
let font_id = FontId::default();
let font_height = fonts.font(&font_id).row_height();
let font_height = fonts
.font(&font_id.family)
.scaled_metrics(pixels_per_point, font_id.size)
.row_height;
let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
let galley = layout(&mut fonts, job.into());
let galley = layout(&mut fonts, pixels_per_point, job.into());
assert_eq!(galley.rows.len(), 2, "Expected two rows");
assert_eq!(

View File

@@ -8,14 +8,14 @@ use super::{
cursor::{CCursor, LayoutCursor},
font::UvRect,
};
use crate::{Color32, FontId, Mesh, Stroke};
use crate::{Color32, FontId, Mesh, Stroke, text::FontsView};
use emath::{Align, GuiRounding as _, NumExt as _, OrderedFloat, Pos2, Rect, Vec2, pos2, vec2};
/// Describes the task of laying out text.
///
/// This supports mixing different fonts, color and formats (underline etc).
///
/// Pass this to [`crate::Fonts::layout_job`] or [`crate::text::layout`].
/// Pass this to [`crate::FontsView::layout_job`] or [`crate::text::layout`].
///
/// ## Example:
/// ```
@@ -184,7 +184,7 @@ impl LayoutJob {
/// The height of the tallest font used in the job.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn font_height(&self, fonts: &crate::Fonts) -> f32 {
pub fn font_height(&self, fonts: &mut FontsView<'_>) -> f32 {
let mut max_height = 0.0_f32;
for section in &self.sections {
max_height = max_height.max(fonts.row_height(&section.format.font_id));
@@ -504,7 +504,7 @@ impl TextWrapping {
/// Text that has been laid out, ready for painting.
///
/// You can create a [`Galley`] using [`crate::Fonts::layout_job`];
/// You can create a [`Galley`] using [`crate::FontsView::layout_job`];
///
/// Needs to be recreated if the underlying font atlas texture changes, which
/// happens under the following conditions: