1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-28 07:23:13 -04:00

Let Fonts handle multiple pixels_per_point

This commit is contained in:
valadaptive
2025-07-04 01:16:55 -04:00
parent 837fc7fef7
commit 9aaee3354d
13 changed files with 218 additions and 263 deletions

View File

@@ -2,15 +2,15 @@
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use emath::{GuiRounding as _, OrderedFloat};
use emath::GuiRounding as _;
use epaint::{
ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, StrokeKind,
TessellationOptions, TextureAtlas, TextureId, Vec2,
ClippedPrimitive, ClippedShape, Color32, ImageData, Pos2, Rect, StrokeKind,
TessellationOptions, TextureId, Vec2,
emath::{self, TSTransform},
mutex::RwLock,
stats::PaintStats,
tessellator,
text::{FontInsert, FontPriority, Fonts},
text::{FontInsert, FontPriority, Fonts, FontsView},
vec2,
};
@@ -406,12 +406,7 @@ impl ViewportRepaintInfo {
#[derive(Default)]
struct ContextImpl {
/// Since we could have multiple viewports across multiple monitors with
/// different `pixels_per_point`, we need a `Fonts` instance for each unique
/// `pixels_per_point`.
/// This is because the `Fonts` depend on `pixels_per_point` for the font atlas
/// as well as kerning, font sizes, etc.
fonts: std::collections::BTreeMap<OrderedFloat<f32>, Fonts>,
fonts: Option<Fonts>,
font_definitions: FontDefinitions,
memory: Memory,
@@ -575,12 +570,11 @@ impl ContextImpl {
fn update_fonts_mut(&mut self) {
profiling::function_scope!();
let input = &self.viewport().input;
let pixels_per_point = input.pixels_per_point();
let max_texture_side = input.max_texture_side;
if let Some(font_definitions) = self.memory.new_font_definitions.take() {
// New font definition loaded, so we need to reload all fonts.
self.fonts.clear();
self.fonts = None;
self.font_definitions = font_definitions;
#[cfg(feature = "log")]
log::trace!("Loading new font definitions");
@@ -589,7 +583,7 @@ impl ContextImpl {
if !self.memory.add_fonts.is_empty() {
let fonts = self.memory.add_fonts.drain(..);
for font in fonts {
self.fonts.clear(); // recreate all the fonts
self.fonts = None; // recreate all the fonts
for family in font.families {
let fam = self
.font_definitions
@@ -614,26 +608,22 @@ impl ContextImpl {
let mut is_new = false;
let fonts = self
.fonts
.entry(pixels_per_point.into())
.or_insert_with(|| {
#[cfg(feature = "log")]
log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}");
let fonts = self.fonts.get_or_insert_with(|| {
#[cfg(feature = "log")]
log::trace!("Creating new Fonts");
is_new = true;
profiling::scope!("Fonts::new");
Fonts::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
});
is_new = true;
profiling::scope!("Fonts::new");
Fonts::new(
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
});
{
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(pixels_per_point, max_texture_side, text_alpha_from_coverage);
fonts.begin_pass(max_texture_side, text_alpha_from_coverage);
}
if is_new && self.memory.options.preload_font_glyphs {
@@ -1052,13 +1042,14 @@ impl Context {
/// Not valid until first call to [`Context::run()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
self.write(move |ctx| {
let pixels_per_point = ctx.pixels_per_point();
reader(
ctx.fonts
.get(&pixels_per_point.into())
.expect("No fonts available until first call to Context::run()"),
&ctx.fonts
.as_mut()
.expect("No fonts available until first call to Context::run()")
.with_pixels_per_point(pixels_per_point),
)
})
}
@@ -1068,13 +1059,15 @@ impl Context {
/// Not valid until first call to [`Context::run()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
self.write(move |ctx| {
let pixels_per_point = ctx.pixels_per_point();
reader(
ctx.fonts
.get_mut(&pixels_per_point.into())
.expect("No fonts available until first call to Context::run()"),
&mut ctx
.fonts
.as_mut()
.expect("No fonts available until first call to Context::run()")
.with_pixels_per_point(pixels_per_point),
)
})
}
@@ -1938,14 +1931,12 @@ impl Context {
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
let mut update_fonts = true;
self.read(|ctx| {
if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) {
if let Some(current_fonts) = ctx.fonts.as_ref() {
// NOTE: this comparison is expensive since it checks TTF data for equality
if current_fonts.fonts.definitions() == &font_definitions {
if current_fonts.definitions() == &font_definitions {
update_fonts = false; // no need to update
}
}
@@ -1966,14 +1957,11 @@ impl Context {
pub fn add_font(&self, new_font: FontInsert) {
profiling::function_scope!();
let pixels_per_point = self.pixels_per_point();
let mut update_fonts = true;
self.read(|ctx| {
if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) {
if let Some(current_fonts) = ctx.fonts.as_ref() {
if current_fonts
.fonts
.definitions()
.font_data
.contains_key(&new_font.name)
@@ -2466,31 +2454,12 @@ impl ContextImpl {
self.memory.end_pass(&viewport.this_pass.used_ids);
let num_font_envs = self.fonts.len();
if let Some(fonts) = self.fonts.get_mut(&pixels_per_point.into()) {
if let Some(fonts) = self.fonts.as_mut() {
let tex_mngr = &mut self.tex_manager.0.write();
if let Some(font_image_delta) = fonts.font_image_delta() {
// A partial font atlas update, e.g. a new glyph has been entered.
tex_mngr.set(TextureId::default(), font_image_delta);
}
if 1 < num_font_envs {
// We have multiple different `pixels_per_point`,
// e.g. because we have many viewports spread across
// monitors with different DPI scaling.
// All viewports share the same texture namespace and renderer,
// so the all use `TextureId::default()` for the font texture.
// This is a problem.
// We solve this with a hack: we always upload the full font atlas
// every frame, for all viewports.
// This ensures it is up-to-date, solving
// https://github.com/emilk/egui/issues/3664
// at the cost of a lot of performance.
// (This will override any smaller delta that was uploaded above.)
profiling::scope!("full_font_atlas_update");
let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options());
tex_mngr.set(TextureId::default(), full_delta);
}
}
// Inform the backend of all textures that have been updated (including font atlas).
@@ -2633,24 +2602,6 @@ impl ContextImpl {
self.memory.set_viewport_id(viewport_id);
}
let active_pixels_per_point: std::collections::BTreeSet<OrderedFloat<f32>> = self
.viewports
.values()
.map(|v| v.input.pixels_per_point.into())
.collect();
self.fonts.retain(|pixels_per_point, _| {
if active_pixels_per_point.contains(pixels_per_point) {
true
} else {
#[cfg(feature = "log")]
log::trace!(
"Freeing Fonts with pixels_per_point={} because it is no longer needed",
pixels_per_point.into_inner()
);
false
}
});
platform_output.num_completed_passes += 1;
FullOutput {
@@ -2682,7 +2633,7 @@ impl Context {
self.write(|ctx| {
let tessellation_options = ctx.memory.options.tessellation_options;
let texture_atlas = if let Some(fonts) = ctx.fonts.get(&pixels_per_point.into()) {
let texture_atlas = if let Some(fonts) = ctx.fonts.as_ref() {
fonts.texture_atlas()
} else {
#[cfg(feature = "log")]
@@ -2691,7 +2642,6 @@ impl Context {
.iter()
.next()
.expect("No fonts loaded")
.1
.texture_atlas()
};

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use emath::GuiRounding as _;
use epaint::{
CircleShape, ClippedShape, CornerRadius, PathStroke, RectShape, Shape, Stroke, StrokeKind,
text::{Fonts, Galley, LayoutJob},
text::{FontsView, Galley, LayoutJob},
};
use crate::{
@@ -145,7 +145,7 @@ impl Painter {
///
/// See [`Context`] documentation for how locks work.
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
self.ctx.fonts(reader)
}
@@ -153,7 +153,7 @@ impl Painter {
///
/// See [`Context`] documentation for how locks work.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
self.ctx.fonts_mut(reader)
}

View File

@@ -3,6 +3,7 @@
use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use epaint::text::FontsView;
use std::{any::Any, hash::Hash, sync::Arc};
use crate::ClosableTag;
@@ -16,9 +17,7 @@ use crate::{
WidgetRect, WidgetText,
containers::{CollapsingHeader, CollapsingResponse, Frame},
ecolor::Hsva,
emath, epaint,
epaint::text::Fonts,
grid,
emath, epaint, grid,
layout::{Direction, Layout},
pass_state,
placer::Placer,
@@ -849,13 +848,13 @@ impl Ui {
/// Read-only access to [`Fonts`].
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&Fonts) -> R) -> R {
pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
self.ctx().fonts(reader)
}
/// Read-write access to [`Fonts`].
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut Fonts) -> R) -> R {
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
self.ctx().fonts_mut(reader)
}
}

View File

@@ -307,7 +307,7 @@ impl RichText {
/// Read the font height of the selected text style.
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 {
pub fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
let mut font_id = self.text_style.as_ref().map_or_else(
|| FontSelection::Default.resolve(style),
|text_style| text_style.resolve(style),
@@ -676,7 +676,7 @@ impl WidgetText {
}
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub(crate) fn font_height(&self, fonts: &mut epaint::Fonts, style: &Style) -> f32 {
pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
match self {
Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
Self::RichText(text) => text.font_height(fonts, style),

View File

@@ -166,7 +166,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let font_id = egui::FontId::default();
let text_color = egui::Color32::WHITE;
let mut fonts = egui::epaint::text::Fonts::new(
pixels_per_point,
max_texture_side,
egui::epaint::AlphaFromCoverage::default(),
egui::FontDefinitions::default(),
@@ -182,13 +181,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
text_color,
wrap_width,
);
layout(&mut fonts.fonts, job.into())
layout(&mut fonts.fonts, job.into(), pixels_per_point)
});
});
}
c.bench_function("text_layout_cached", |b| {
b.iter(|| {
fonts.layout(
fonts.with_pixels_per_point(pixels_per_point).layout(
LOREM_IPSUM_LONG.to_owned(),
font_id.clone(),
text_color,
@@ -210,22 +209,28 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let mut rng = rand::rng();
b.iter(|| {
fonts.begin_pass(
pixels_per_point,
max_texture_side,
egui::epaint::AlphaFromCoverage::default(),
);
fonts.begin_pass(max_texture_side, egui::epaint::AlphaFromCoverage::default());
// Delete a random character, simulating a user making an edit in a long file:
let mut new_string = string.clone();
let idx = rng.random_range(0..string.len());
new_string.remove(idx);
fonts.layout(new_string, font_id.clone(), text_color, wrap_width);
fonts.with_pixels_per_point(pixels_per_point).layout(
new_string,
font_id.clone(),
text_color,
wrap_width,
);
});
});
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width);
let galley = fonts.with_pixels_per_point(pixels_per_point).layout(
LOREM_IPSUM_LONG.to_owned(),
font_id,
text_color,
wrap_width,
);
let font_image_size = fonts.font_image_size();
let prepared_discs = fonts.texture_atlas().prepared_discs();
let mut tessellator = egui::epaint::Tessellator::new(

View File

@@ -62,7 +62,7 @@ pub use self::{
stats::PaintStats,
stroke::{PathStroke, Stroke, StrokeKind},
tessellator::{TessellationOptions, Tessellator},
text::{FontFamily, FontId, Fonts, Galley},
text::{FontFamily, FontId, Fonts, FontsView, Galley},
texture_atlas::TextureAtlas,
texture_handle::TextureHandle,
textures::TextureManager,

View File

@@ -7,7 +7,7 @@ use emath::{Align2, Pos2, Rangef, Rect, TSTransform, Vec2, pos2};
use crate::{
Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId,
stroke::PathStroke,
text::{FontId, Fonts, Galley},
text::{FontId, FontsView, Galley},
};
use super::{
@@ -299,7 +299,7 @@ impl Shape {
#[expect(clippy::needless_pass_by_value)]
pub fn text(
fonts: &mut Fonts,
fonts: &mut FontsView<'_>,
pos: Pos2,
anchor: Align2,
text: impl ToString,

View File

@@ -182,7 +182,6 @@ mod tests {
#[test]
fn text_bounding_box_under_rotation() {
let mut fonts = Fonts::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -190,7 +189,7 @@ mod tests {
let font = FontId::monospace(12.0);
let mut t = crate::Shape::text(
&mut fonts,
&mut fonts.with_pixels_per_point(1.0),
Pos2::ZERO,
emath::Align2::CENTER_CENTER,
"testing123",

View File

@@ -155,7 +155,7 @@ impl FontImpl {
}
/// `\n` will result in `None`
fn glyph_info(&mut self, c: char) -> Option<GlyphInfo> {
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);
@@ -424,7 +424,9 @@ impl Font<'_> {
return *font_index_glyph_info;
}
let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c);
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
@@ -471,25 +473,6 @@ impl Font<'_> {
self.row_height(font_size)
}
}
pub(crate) fn glyph_info_no_cache_or_fallback(
&mut self,
c: char,
) -> Option<(FontFaceKey, GlyphInfo)> {
for font_key in &self.cached_family.fonts {
let font_impl = self
.fonts_by_id
.get_mut(font_key)
.expect("Nonexistent font ID");
if let Some(glyph_info) = font_impl.glyph_info(c) {
self.cached_family
.glyph_info_cache
.insert(c, (*font_key, glyph_info));
return Some((*font_key, glyph_info));
}
}
None
}
}
/// Code points that will always be invisible (zero width).

View File

@@ -434,7 +434,6 @@ impl CachedFamily {
fn new(
fonts: Vec<FontFaceKey>,
fonts_by_id: &mut nohash_hasher::IntMap<FontFaceKey, FontImpl>,
atlas: &mut TextureAtlas,
) -> Self {
if fonts.is_empty() {
return Self {
@@ -455,15 +454,9 @@ impl CachedFamily {
const PRIMARY_REPLACEMENT_CHAR: char = '◻'; // white medium square
const FALLBACK_REPLACEMENT_CHAR: char = '?'; // fallback for the fallback
let mut font = Font {
fonts_by_id,
cached_family: &mut slf,
atlas,
};
let replacement_glyph = font
.glyph_info_no_cache_or_fallback(PRIMARY_REPLACEMENT_CHAR)
.or_else(|| font.glyph_info_no_cache_or_fallback(FALLBACK_REPLACEMENT_CHAR))
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!(
@@ -475,6 +468,21 @@ impl CachedFamily {
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
}
}
// ----------------------------------------------------------------------------
@@ -497,21 +505,14 @@ 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 {
Self {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
galley_cache: Default::default(),
}
}
@@ -525,30 +526,21 @@ impl Fonts {
/// as well as notice when the font atlas is getting full, and handle that.
pub fn begin_pass(
&mut self,
pixels_per_point: f32,
max_texture_side: usize,
text_alpha_from_coverage: AlphaFromCoverage,
) {
let pixels_per_point_changed = self.fonts.pixels_per_point != pixels_per_point;
let max_texture_side_changed = self.fonts.max_texture_side != max_texture_side;
let text_alpha_from_coverage_changed =
self.fonts.atlas.text_alpha_from_coverage != text_alpha_from_coverage;
let font_atlas_almost_full = self.fonts.atlas.fill_ratio() > 0.8;
let needs_recreate = pixels_per_point_changed
|| max_texture_side_changed
|| text_alpha_from_coverage_changed
|| font_atlas_almost_full;
let needs_recreate =
max_texture_side_changed || text_alpha_from_coverage_changed || font_atlas_almost_full;
if needs_recreate {
let definitions = self.fonts.definitions.clone();
*self = Self {
fonts: FontsImpl::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
definitions,
),
fonts: FontsImpl::new(max_texture_side, text_alpha_from_coverage, definitions),
galley_cache: Default::default(),
};
}
@@ -562,13 +554,13 @@ impl Fonts {
}
#[inline]
pub fn pixels_per_point(&self) -> f32 {
self.fonts.pixels_per_point
pub fn max_texture_side(&self) -> usize {
self.fonts.max_texture_side
}
#[inline]
pub fn max_texture_side(&self) -> usize {
self.fonts.max_texture_side
pub fn definitions(&self) -> &FontDefinitions {
&self.fonts.definitions
}
/// The font atlas.
@@ -589,29 +581,92 @@ impl Fonts {
self.fonts.atlas.size()
}
/// Width of this character in points.
#[inline]
pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
self.fonts.glyph_width(font_id, c)
}
/// Can we display this glyph?
#[inline]
pub fn has_glyph(&mut self, font_id: &FontId, c: char) -> bool {
self.fonts.has_glyph(font_id, c)
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.has_glyphs(font_id, s)
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(&mut self, font_id: &FontId) -> f32 {
self.fonts.row_height(font_id)
self.fonts.font(&font_id.family).row_height(font_id.size)
}
/// List of all known font families.
@@ -629,8 +684,12 @@ impl Fonts {
#[inline]
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(&mut self.fonts, job, allow_split_paragraphs)
self.galley_cache.layout(
self.fonts,
job,
self.pixels_per_point,
allow_split_paragraphs,
)
}
pub fn num_galleys_in_cache(&self) -> usize {
@@ -687,13 +746,10 @@ impl Fonts {
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
/// 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: TextureAtlas,
@@ -706,16 +762,10 @@ 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);
@@ -732,7 +782,6 @@ impl FontsImpl {
}
Self {
pixels_per_point,
max_texture_side,
definitions,
atlas,
@@ -742,16 +791,6 @@ impl FontsImpl {
}
}
#[inline(always)]
pub fn pixels_per_point(&self) -> f32 {
self.pixels_per_point
}
#[inline]
pub fn definitions(&self) -> &FontDefinitions {
&self.definitions
}
/// 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(|| {
@@ -769,7 +808,7 @@ impl FontsImpl {
})
.collect();
CachedFamily::new(fonts, &mut self.fonts_by_id, &mut self.atlas)
CachedFamily::new(fonts, &mut self.fonts_by_id)
});
Font {
fonts_by_id: &mut self.fonts_by_id,
@@ -777,28 +816,6 @@ impl FontsImpl {
atlas: &mut self.atlas,
}
}
/// Width of this character in points.
fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
self.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.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.font(&font_id.family).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.family).row_height(font_id.size)
}
}
// ----------------------------------------------------------------------------
@@ -827,6 +844,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() {
@@ -882,14 +900,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,
@@ -901,7 +918,7 @@ impl GalleyCache {
);
galley
} else {
let galley = super::layout(fonts, job);
let galley = super::layout(fonts, job, pixels_per_point);
let galley = Arc::new(galley);
entry.insert(CachedGalley {
last_used: self.generation,
@@ -920,9 +937,11 @@ impl GalleyCache {
&mut self,
fonts: &mut FontsImpl,
job: LayoutJob,
pixels_per_point: f32,
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.
@@ -930,6 +949,7 @@ impl GalleyCache {
&mut self,
fonts: &mut FontsImpl,
job: &LayoutJob,
pixels_per_point: f32,
) -> (Vec<Arc<Galley>>, Vec<u64>) {
profiling::function_scope!();
@@ -1009,7 +1029,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:
@@ -1164,7 +1185,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(),
@@ -1176,9 +1196,11 @@ 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, job.clone(), 1., false);
let split = GalleyCache::default().layout(&mut fonts, job.clone(), true);
let split =
GalleyCache::default().layout(&mut fonts, job.clone(), 1., true);
for (i, row) in whole.rows.iter().enumerate() {
println!(
@@ -1217,7 +1239,6 @@ mod tests {
for pixels_per_point in pixels_per_point {
let mut fonts = FontsImpl::new(
pixels_per_point,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1230,12 +1251,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, job.clone().into(), pixels_per_point);
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, job.into(), pixels_per_point);
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

@@ -68,7 +68,7 @@ impl Paragraph {
///
/// In most cases you should use [`crate::Fonts::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, job: Arc<LayoutJob>, pixels_per_point: f32) -> Galley {
profiling::function_scope!();
if job.wrap.max_rows == 0 {
@@ -80,7 +80,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 +90,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,
&job,
pixels_per_point,
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 +109,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, &job, last_row, pixels_per_point);
if let Some(last) = last_row.glyphs.last() {
last_row.size.x = last.max_x();
}
@@ -134,6 +141,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
fn layout_section(
fonts: &mut FontsImpl,
job: &LayoutJob,
pixels_per_point: f32,
section_index: u32,
section: &LayoutSection,
out_paragraphs: &mut Vec<Paragraph>,
@@ -143,7 +151,6 @@ fn layout_section(
byte_range,
format,
} = section;
let pixels_per_point = fonts.pixels_per_point();
let mut font = fonts.font(&format.font_id.family);
let font_size = format.font_id.size;
let line_height = section
@@ -407,6 +414,7 @@ fn replace_last_glyph_with_overflow_character(
fonts: &mut FontsImpl,
job: &LayoutJob,
row: &mut Row,
pixels_per_point: f32,
) {
fn row_width(row: &Row) -> f32 {
if let (Some(first), Some(last)) = (row.glyphs.first(), row.glyphs.last()) {
@@ -427,8 +435,6 @@ fn replace_last_glyph_with_overflow_character(
return;
};
let pixels_per_point = fonts.pixels_per_point();
// We always try to just append the character first:
if let Some(last_glyph) = row.glyphs.last() {
let section_index = last_glyph.section_index;
@@ -514,7 +520,6 @@ fn replace_last_glyph_with_overflow_character(
let section = &job.sections[last_glyph.section_index as usize];
let extra_letter_spacing = section.format.extra_letter_spacing;
let pixels_per_point = fonts.pixels_per_point();
let mut font = fonts.font(&section.format.font_id.family);
let font_size = section.format.font_id.size;
@@ -568,7 +573,6 @@ fn replace_last_glyph_with_overflow_character(
let Some(section) = &job.sections.get(last_glyph.section_index as usize) else {
return;
};
let pixels_per_point = fonts.pixels_per_point();
let mut font = fonts.font(&section.format.font_id.family);
let font_size = section.format.font_id.size;
// Just replace and be done with it.
@@ -1119,14 +1123,13 @@ mod tests {
#[test]
fn test_zero_max_width() {
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, layout_job.into(), 1.0);
assert_eq!(galley.rows.len(), 1);
}
@@ -1135,7 +1138,6 @@ mod tests {
// No matter where we wrap, we should be appending the newline character.
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1154,7 +1156,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, layout_job.into(), 1.0);
assert!(galley.elided);
assert_eq!(galley.rows.len(), 1);
@@ -1173,7 +1175,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, layout_job.into(), 1.0);
assert!(galley.elided);
assert_eq!(galley.rows.len(), 1);
@@ -1185,7 +1187,6 @@ mod tests {
#[test]
fn test_cjk() {
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1195,7 +1196,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, layout_job.into(), 1.0);
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
vec!["日本語と", "Englishの混在", "した文章"]
@@ -1205,7 +1206,6 @@ mod tests {
#[test]
fn test_pre_cjk() {
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1215,7 +1215,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, layout_job.into(), 1.0);
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
vec!["日本語とEnglish", "の混在した文章"]
@@ -1225,7 +1225,6 @@ mod tests {
#[test]
fn test_truncate_width() {
let mut fonts = FontsImpl::new(
1.0,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
@@ -1235,7 +1234,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, layout_job.into(), 1.0);
assert!(galley.elided);
assert_eq!(
galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
@@ -1249,18 +1248,17 @@ mod tests {
#[test]
fn test_empty_row() {
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).row_height(font_id.size);
let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
let galley = layout(&mut fonts, job.into());
let galley = layout(&mut fonts, job.into(), 1.0);
assert_eq!(galley.rows.len(), 1, "Expected one row");
assert_eq!(
@@ -1283,18 +1281,17 @@ mod tests {
#[test]
fn test_end_with_newline() {
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).row_height(font_id.size);
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, job.into(), 1.0);
assert_eq!(galley.rows.len(), 2, "Expected two rows");
assert_eq!(

View File

@@ -8,7 +8,7 @@ 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.
@@ -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: &mut 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));