Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bbf9ac4
Stop wrapping FontsImpl in an Arc<Mutex<_>>
valadaptive Jul 3, 2025
955e678
Remove FontTweak::baseline_offset_factor
valadaptive Jul 3, 2025
02d45d7
Make Font+FontImpl resolution and size independent
valadaptive Jul 4, 2025
837fc7f
Remove more Arc<Mutex<...>> from font code
valadaptive Jul 4, 2025
9aaee33
Let `Fonts` handle multiple `pixels_per_point`
valadaptive Jul 4, 2025
a113907
Include pixels_per_point in galley cache key
valadaptive Jul 5, 2025
9a77409
Rename font_impls field to fonts_by_name
valadaptive Aug 7, 2025
52ce61a
Please the linter
valadaptive Aug 7, 2025
e5a2239
Fix docs
valadaptive Aug 7, 2025
1ddd143
Fix panic if no fonts loaded
valadaptive Aug 7, 2025
560346d
add docstring
emilk Sep 5, 2025
3385ad5
Pre-hash contents of FontFaceKey
emilk Sep 5, 2025
0d628e6
Change parameter order and fix bug in test
emilk Sep 5, 2025
9d8dba4
Update test output
emilk Sep 5, 2025
686f070
Fix benchmark compilation
emilk Sep 5, 2025
6f53261
Refactor `GlyphInfo`
emilk Sep 5, 2025
e372ffe
Update to latest ab_glyph
emilk Sep 5, 2025
58cbca1
Change argument order
emilk Sep 5, 2025
1ccd809
Fix glyph cache key
valadaptive Aug 8, 2025
3a6cb2e
Refactor `replace_last_glyph_with_overflow_character`
valadaptive Sep 7, 2025
f560b6a
Cache scaled font metrics
valadaptive Sep 7, 2025
95458f4
Stop using ab_glyph scaling entirely
valadaptive Sep 8, 2025
38fbae0
Cache GlyphIds for allocation instead of full info
valadaptive Sep 8, 2025
4325c8f
Optimize glyph allocation cache lookups
valadaptive Sep 8, 2025
91d3384
Remove busted glyph-centering code
valadaptive Sep 8, 2025
dcb6420
Update inaccurate comment
valadaptive Sep 8, 2025
03a4f2c
Stop accumulating rounding error on purpose
valadaptive Sep 8, 2025
d659df6
Subpixel glyph positioning
valadaptive Sep 8, 2025
abe9a2d
Apply review suggestion about GlyphCacheKey
valadaptive Sep 8, 2025
61351d1
Apply documentation changes
valadaptive Sep 8, 2025
623c4c6
Disable subpixel binning for CJK glyphs
valadaptive Sep 8, 2025
237d217
Fix bespoke formatting lints
valadaptive Sep 8, 2025
4e15f53
Better naming + bug fix (wrong unit)
emilk Sep 8, 2025
45a1096
Small naming and docstring improvements
emilk Sep 8, 2025
80c00df
Simplify kerning code
emilk Sep 8, 2025
5b3c80a
Small code cleanup
emilk Sep 8, 2025
197ec85
Fix glyph width of tab and thin space
emilk Sep 8, 2025
b00f528
Merge branch 'main' into fonts-cleanup
emilk Sep 8, 2025
df33830
Update snapshot images
emilk Sep 8, 2025
6a6033a
Missed one (somehow)
emilk Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ version = 4

[[package]]
name = "ab_glyph"
version = "0.2.29"
version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser",
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/atomics/atom_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub trait AtomExt<'a> {
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts(|f| f.row_height(&font_id));
let height = ui.fonts_mut(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ impl Window<'_> {
let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
let style = ctx.style();
let title_bar_inner_height = ctx
.fonts(|fonts| title.font_height(fonts, &style))
.fonts_mut(|fonts| title.font_height(fonts, &style))
.at_least(style.spacing.interact_size.y);
let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
let half_height = (title_bar_inner_height / 2.0).round() as _;
Expand Down
148 changes: 56 additions & 92 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand All @@ -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
Expand All @@ -614,34 +608,33 @@ 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}");

is_new = true;
profiling::scope!("Fonts::new");
Fonts::new(
pixels_per_point,
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
});
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(
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 {
profiling::scope!("preload_font_glyphs");
// Preload the most common characters for the most common fonts.
// This is not very important to do, but may save a few GPU operations.
for font_id in self.memory.options.style().text_styles.values() {
fonts.lock().fonts.font(font_id).preload_common_characters();
fonts
.fonts
.font(&font_id.family)
.preload_common_characters();
}
}
}
Expand Down Expand Up @@ -1049,13 +1042,32 @@ 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),
)
})
}

/// Read-write access to [`Fonts`].
///
/// 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 FontsView<'_>) -> R) -> R {
self.write(move |ctx| {
let pixels_per_point = ctx.pixels_per_point();
reader(
&mut ctx
.fonts
.as_mut()
.expect("No fonts available until first call to Context::run()")
.with_pixels_per_point(pixels_per_point),
)
})
}
Expand Down Expand Up @@ -1568,9 +1580,8 @@ impl Context {
} = ModifierNames::SYMBOLS;

let font_id = TextStyle::Body.resolve(&self.style());
self.fonts(|f| {
let mut lock = f.lock();
let font = lock.fonts.font(&font_id);
self.fonts_mut(|f| {
let mut font = f.fonts.font(&font_id.family);
font.has_glyphs(alt)
&& font.has_glyphs(ctrl)
&& font.has_glyphs(shift)
Expand Down Expand Up @@ -1920,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.lock().fonts.definitions() == &font_definitions {
if current_fonts.definitions() == &font_definitions {
update_fonts = false; // no need to update
}
}
Expand All @@ -1948,15 +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
.lock()
.fonts
.definitions()
.font_data
.contains_key(&new_font.name)
Expand Down Expand Up @@ -2449,30 +2454,12 @@ impl ContextImpl {

self.memory.end_pass(&viewport.this_pass.used_ids);

if let Some(fonts) = self.fonts.get(&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 < self.fonts.len() {
// 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).
Expand Down Expand Up @@ -2615,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 {
Expand Down Expand Up @@ -2664,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")]
Expand All @@ -2673,22 +2642,17 @@ impl Context {
.iter()
.next()
.expect("No fonts loaded")
.1
.texture_atlas()
};
let (font_tex_size, prepared_discs) = {
let atlas = texture_atlas.lock();
(atlas.size(), atlas.prepared_discs())
};

let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_primitives = {
profiling::scope!("tessellator::tessellate_shapes");
tessellator::Tessellator::new(
pixels_per_point,
tessellation_options,
font_tex_size,
prepared_discs,
texture_atlas.size(),
texture_atlas.prepared_discs(),
)
.tessellate_shapes(shapes)
};
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/debug_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl State {
{
// Paint location to left of `pos`:
let location_galley =
ctx.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
ctx.fonts_mut(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
let location_rect =
Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size());
painter.galley(location_rect.min, location_galley, color);
Expand Down
20 changes: 14 additions & 6 deletions crates/egui/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -141,14 +141,22 @@ impl Painter {
self.pixels_per_point
}

/// Read-only access to the shared [`Fonts`].
/// Read-only access to the shared [`FontsView`].
///
/// 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)
}

/// Read-write access to the shared [`FontsView`].
///
/// See [`Context`] documentation for how locks work.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
self.ctx.fonts_mut(reader)
}

/// Where we paint
#[inline]
pub fn layer_id(&self) -> LayerId {
Expand Down Expand Up @@ -525,7 +533,7 @@ impl Painter {
color: crate::Color32,
wrap_width: f32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, wrap_width))
self.fonts_mut(|f| f.layout(text, font_id, color, wrap_width))
}

/// Will line break at `\n`.
Expand All @@ -539,7 +547,7 @@ impl Painter {
font_id: FontId,
color: crate::Color32,
) -> Arc<Galley> {
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
self.fonts_mut(|f| f.layout(text, font_id, color, f32::INFINITY))
}

/// Lay out this text layut job in a galley.
Expand All @@ -548,7 +556,7 @@ impl Painter {
#[inline]
#[must_use]
pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
self.fonts(|f| f.layout_job(layout_job))
self.fonts_mut(|f| f.layout_job(layout_job))
}

/// Paint text that has already been laid out in a [`Galley`].
Expand Down
Loading
Loading