1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 07:10: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

@@ -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)
}
}

View File

@@ -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 _;

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 {
@@ -641,7 +631,10 @@ impl ContextImpl {
// 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();
}
}
}
@@ -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),
)
})
}
@@ -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)
@@ -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
}
}
@@ -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)
@@ -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).
@@ -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 {
@@ -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")]
@@ -2673,13 +2642,8 @@ 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 = {
@@ -2687,8 +2651,8 @@ impl Context {
tessellator::Tessellator::new(
pixels_per_point,
tessellation_options,
font_tex_size,
prepared_discs,
texture_atlas.size(),
texture_atlas.prepared_discs(),
)
.tessellate_shapes(shapes)
};

View File

@@ -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);

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::{
@@ -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 {
@@ -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`.
@@ -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.
@@ -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`].

View File

@@ -2790,7 +2790,6 @@ impl Widget for &mut FontTweak {
scale,
y_offset_factor,
y_offset,
baseline_offset_factor,
} = self;
ui.label("Scale");
@@ -2806,10 +2805,6 @@ impl Widget for &mut FontTweak {
ui.add(DragValue::new(y_offset).speed(-0.02));
ui.end_row();
ui.label("baseline_offset_factor");
ui.add(DragValue::new(baseline_offset_factor).speed(-0.0025));
ui.end_row();
if ui.button("Reset").clicked() {
*self = Default::default();
}

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,
@@ -735,7 +734,7 @@ impl Ui {
///
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub fn text_style_height(&self, style: &TextStyle) -> f32 {
self.fonts(|f| f.row_height(&style.resolve(self.style())))
self.fonts_mut(|f| f.row_height(&style.resolve(self.style())))
}
/// Screen-space rectangle for clipping what we paint in this ui.
@@ -847,11 +846,17 @@ impl Ui {
self.ctx().output_mut(writer)
}
/// Read-only access to [`Fonts`].
/// Read-only access to [`FontsView`].
#[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 [`FontsView`].
#[inline]
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: &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: &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),
@@ -762,7 +762,7 @@ impl WidgetText {
},
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
ctx.fonts_mut(|f| f.layout_job(layout_job))
}
Self::RichText(text) => {
let mut layout_job = Arc::unwrap_or_clone(text).into_layout_job(
@@ -771,12 +771,12 @@ impl WidgetText {
default_valign,
);
layout_job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(layout_job))
ctx.fonts_mut(|f| f.layout_job(layout_job))
}
Self::LayoutJob(job) => {
let mut job = Arc::unwrap_or_clone(job);
job.wrap = text_wrapping;
ctx.fonts(|f| f.layout_job(job))
ctx.fonts_mut(|f| f.layout_job(job))
}
Self::Galley(galley) => galley,
}

View File

@@ -211,7 +211,7 @@ impl Label {
if let Some(first_section) = layout_job.sections.first_mut() {
first_section.leading_space = first_row_indentation;
}
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let pos = pos2(ui.max_rect().left(), ui.cursor().top());
assert!(!galley.rows.is_empty(), "Galleys are never empty");
@@ -252,7 +252,7 @@ impl Label {
layout_job.justify = ui.layout().horizontal_justify();
}
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
response.intrinsic_size = Some(galley.intrinsic_size());
let galley_pos = match galley.job.halign {

View File

@@ -266,7 +266,7 @@ impl<'t> TextEdit<'t> {
/// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str());
/// layout_job.wrap.max_width = wrap_width;
/// ui.fonts(|f| f.layout_job(layout_job))
/// ui.fonts_mut(|f| f.layout_job(layout_job))
/// };
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
/// # });
@@ -504,7 +504,7 @@ impl TextEdit<'_> {
let hint_text_str = hint_text.text().to_owned();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id));
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
@@ -522,7 +522,7 @@ impl TextEdit<'_> {
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
};
ui.fonts(|f| f.layout_job(layout_job))
ui.fonts_mut(|f| f.layout_job(layout_job))
};
let layouter = layouter.unwrap_or(&mut default_layouter);