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

Replace ab_glyph with Skrifa + vello_cpu; enable font hinting (#7694)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes N/A
* [x] I have followed the instructions in the PR template

I'll probably come back to this and clean it up a bit. This PR
reimplements ab_glyph's functionality on top of Skrifa, a somewhat
lower-level font API that's being used in Chrome now.

Skrifa doesn't perform rasterization itself, so I'm using
[vello_cpu](https://github.com/linebender/vello) from the Linebender
project for rasterization. It's still in its early days, but I believe
it's already quite fast. It also supports color and gradient fills, so
color emoji support will be easier.

Skrifa also supports font hinting, which should make text look a bit
nicer / less blurry.

Here's the current ab_glyph rendering:

<img width="1592" height="1068" alt="image"
src="https://github.com/user-attachments/assets/2385b66e-23f8-4c6e-b8c2-ea90e0eea4e4"
/>

Here's Skrifa *without* hinting--it looks almost identical, but there
are some subpixel differences, probably due to rasterizer behavior:

<img width="1592" height="1068" alt="image"
src="https://github.com/user-attachments/assets/a815f3e9-65ac-4940-bc00-571177bef53d"
/>

Here's Skrifa  *with* hinting:

<img width="1592" height="1068" alt="image"
src="https://github.com/user-attachments/assets/d6cc0669-3537-4377-bba9-ed5ef09664db"
/>

Hinting does make the horizontal strokes look a bit bolder, which makes
me wonder once again about increasing the font weight from "light" to
"regular".

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
valadaptive
2025-12-06 10:11:33 -05:00
committed by GitHub
parent 2174b309bd
commit 609dd2d28e
172 changed files with 928 additions and 643 deletions

View File

@@ -20,7 +20,7 @@ use crate::{
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet,
ViewportOutput, Widget as _, WidgetRect, WidgetText,
ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText,
animation_manager::AnimationManager,
containers::{self, area::AreaState},
data::output::PlatformOutput,
@@ -34,8 +34,7 @@ use crate::{
os::OperatingSystem,
output::FullOutput,
pass_state::PassState,
plugin,
plugin::TypedPluginHandle,
plugin::{self, TypedPluginHandle},
resize, response, scroll_area,
util::IdTypeMap,
viewport::ViewportClass,
@@ -564,7 +563,10 @@ impl ContextImpl {
log::trace!("Adding new fonts");
}
let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage;
let Visuals {
mut text_options, ..
} = self.memory.options.style().visuals;
text_options.max_texture_side = max_texture_side;
let mut is_new = false;
@@ -573,16 +575,12 @@ impl ContextImpl {
is_new = true;
profiling::scope!("Fonts::new");
Fonts::new(
max_texture_side,
text_alpha_from_coverage,
self.font_definitions.clone(),
)
Fonts::new(text_options, self.font_definitions.clone())
});
{
profiling::scope!("Fonts::begin_pass");
fonts.begin_pass(max_texture_side, text_alpha_from_coverage);
fonts.begin_pass(text_options);
}
}
@@ -2006,15 +2004,12 @@ impl Context {
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
profiling::function_scope!();
let mut update_fonts = true;
self.read(|ctx| {
if let Some(current_fonts) = ctx.fonts.as_ref() {
// NOTE: this comparison is expensive since it checks TTF data for equality
if current_fonts.definitions() == &font_definitions {
update_fonts = false; // no need to update
}
}
let update_fonts = self.read(|ctx| {
// NOTE: this comparison is expensive since it checks TTF data for equality
// TODO(valadaptive): add_font only checks the *names* for equality. Change this?
ctx.fonts
.as_ref()
.is_none_or(|fonts| fonts.definitions() != &font_definitions)
});
if update_fonts {

View File

@@ -3,7 +3,7 @@
#![allow(clippy::if_same_then_else)]
use emath::Align;
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, text::FontTweak};
use epaint::{AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions, text::FontTweak};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use crate::{
@@ -948,8 +948,11 @@ pub struct Visuals {
/// this is more to provide a convenient summary of the rest of the settings.
pub dark_mode: bool,
/// ADVANCED: Controls how we render text.
pub text_alpha_from_coverage: AlphaFromCoverage,
/// Controls how we render text.
///
/// The [`TextOptions::max_texture_side`] is ignored and overruled by
/// [`crate::RawInput::max_texture_side`].
pub text_options: TextOptions,
/// Override default text color for all text.
///
@@ -1407,7 +1410,10 @@ impl Visuals {
pub fn dark() -> Self {
Self {
dark_mode: true,
text_alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
text_options: TextOptions {
alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
..Default::default()
},
override_text_color: None,
weak_text_alpha: 0.6,
weak_text_color: None,
@@ -1470,7 +1476,10 @@ impl Visuals {
pub fn light() -> Self {
Self {
dark_mode: false,
text_alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
text_options: TextOptions {
alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
..Default::default()
},
widgets: Widgets::light(),
selection: Selection::light(),
hyperlink_color: Color32::from_rgb(0, 155, 255),
@@ -2107,7 +2116,7 @@ impl Visuals {
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
dark_mode,
text_alpha_from_coverage,
text_options,
override_text_color: _,
weak_text_alpha,
weak_text_color,
@@ -2207,7 +2216,7 @@ impl Visuals {
});
});
ui.collapsing("Text color", |ui| {
ui.collapsing("Text rendering", |ui| {
fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into<RichText>) {
ui.label(label.into().color(*color));
ui.color_edit_button_srgba(color);
@@ -2259,7 +2268,15 @@ impl Visuals {
ui.add_space(4.0);
text_alpha_from_coverage_ui(ui, text_alpha_from_coverage);
let TextOptions {
max_texture_side: _,
alpha_from_coverage,
font_hinting,
} = text_options;
text_alpha_from_coverage_ui(ui, alpha_from_coverage);
ui.checkbox(font_hinting, "Enable font hinting");
});
ui.collapsing("Text cursor", |ui| {
@@ -2370,9 +2387,9 @@ impl Visuals {
}
}
fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut AlphaFromCoverage) {
fn text_alpha_from_coverage_ui(ui: &mut Ui, alpha_from_coverage: &mut AlphaFromCoverage) {
let mut dark_mode_special =
*text_alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
*alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
ui.horizontal(|ui| {
ui.label("Text rendering:");
@@ -2380,9 +2397,9 @@ fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut Alpha
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
if dark_mode_special {
*text_alpha_from_coverage = AlphaFromCoverage::TwoCoverageMinusCoverageSq;
*alpha_from_coverage = AlphaFromCoverage::DARK_MODE_DEFAULT;
} else {
let mut gamma = match text_alpha_from_coverage {
let mut gamma = match alpha_from_coverage {
AlphaFromCoverage::Linear => 1.0,
AlphaFromCoverage::Gamma(gamma) => *gamma,
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
@@ -2396,9 +2413,9 @@ fn text_alpha_from_coverage_ui(ui: &mut Ui, text_alpha_from_coverage: &mut Alpha
);
if gamma == 1.0 {
*text_alpha_from_coverage = AlphaFromCoverage::Linear;
*alpha_from_coverage = AlphaFromCoverage::Linear;
} else {
*text_alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
*alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
}
}
});
@@ -2812,6 +2829,7 @@ impl Widget for &mut FontTweak {
scale,
y_offset_factor,
y_offset,
hinting_override,
} = self;
ui.label("Scale");
@@ -2827,6 +2845,19 @@ impl Widget for &mut FontTweak {
ui.add(DragValue::new(y_offset).speed(-0.02));
ui.end_row();
ui.label("hinting_override");
ComboBox::from_id_salt("hinting_override")
.selected_text(match hinting_override {
None => "None",
Some(true) => "Enable",
Some(false) => "Disable",
})
.show_ui(ui, |ui| {
ui.selectable_value(hinting_override, None, "None");
ui.selectable_value(hinting_override, Some(true), "Enable");
ui.selectable_value(hinting_override, Some(false), "Disable");
});
if ui.button("Reset").clicked() {
*self = Default::default();
}