Skip to content

Commit 6ef0c74

Browse files
committed
Add font hinting API
1 parent 31dd626 commit 6ef0c74

File tree

7 files changed

+116
-32
lines changed

7 files changed

+116
-32
lines changed

crates/egui/src/context.rs

+34-21
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,10 @@ impl ContextImpl {
612612
.expect("new_font_definitions is borrowed, but we had nowhere to borrow it from")
613613
};
614614

615+
if let Some(new_font_hinting) = self.memory.new_font_hinting.take() {
616+
fonts.set_hinting_enabled(new_font_hinting);
617+
}
618+
615619
{
616620
profiling::scope!("FontStore::begin_pass");
617621
fonts.begin_pass(max_texture_side);
@@ -1801,15 +1805,12 @@ impl Context {
18011805
pub fn set_fonts(&self, font_definitions: FontDefinitions) {
18021806
profiling::function_scope!();
18031807

1804-
let mut update_fonts = true;
1805-
1806-
self.read(|ctx| {
1807-
if let Some(current_fonts) = ctx.fonts.as_ref() {
1808-
// NOTE: this comparison is expensive since it checks TTF data for equality
1809-
if current_fonts.definitions() == &font_definitions {
1810-
update_fonts = false; // no need to update
1811-
}
1812-
}
1808+
let update_fonts = self.read(|ctx| {
1809+
// NOTE: this comparison is expensive since it checks TTF data for equality
1810+
// TODO(valadaptive): add_font only checks the *names* for equality. Change this?
1811+
ctx.fonts
1812+
.as_ref()
1813+
.is_none_or(|fonts| fonts.definitions() != &font_definitions)
18131814
});
18141815

18151816
if update_fonts {
@@ -1827,25 +1828,37 @@ impl Context {
18271828
pub fn add_font(&self, new_font: FontInsert) {
18281829
profiling::function_scope!();
18291830

1830-
let mut update_fonts = true;
1831-
1832-
self.read(|ctx| {
1833-
if let Some(current_fonts) = ctx.fonts.as_ref() {
1834-
if current_fonts
1835-
.definitions()
1836-
.font_data
1837-
.contains_key(&new_font.name)
1838-
{
1839-
update_fonts = false; // no need to update
1840-
}
1841-
}
1831+
let update_fonts = self.read(|ctx| {
1832+
ctx.fonts
1833+
.as_ref()
1834+
.is_none_or(|fonts| !fonts.definitions().font_data.contains_key(&new_font.name))
18421835
});
18431836

18441837
if update_fonts {
18451838
self.memory_mut(|mem| mem.add_fonts.push(new_font));
18461839
}
18471840
}
18481841

1842+
/// Set whether font hinting (pixel snapping for clearer fonts) is enabled.
1843+
///
1844+
/// By default, hinting is enabled. You can override this per-font with
1845+
/// [`crate::FontTweak::hinting_enabled`].
1846+
///
1847+
/// The new font hinting setting will become active at the start of the next pass.
1848+
pub fn set_font_hinting_enabled(&self, hinting_enabled: bool) {
1849+
profiling::function_scope!();
1850+
1851+
let update_hinting = self.read(|ctx| {
1852+
ctx.fonts
1853+
.as_ref()
1854+
.is_none_or(|fonts| fonts.hinting_enabled() != hinting_enabled)
1855+
});
1856+
1857+
if update_hinting {
1858+
self.memory_mut(|mem| mem.new_font_hinting = Some(hinting_enabled));
1859+
}
1860+
}
1861+
18491862
/// Does the OS use dark or light mode?
18501863
/// This is used when the theme preference is set to [`crate::ThemePreference::System`].
18511864
pub fn system_theme(&self) -> Option<Theme> {

crates/egui/src/memory/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ pub struct Memory {
7979
#[cfg_attr(feature = "persistence", serde(skip))]
8080
pub(crate) new_font_definitions: Option<epaint::text::FontDefinitions>,
8181

82+
/// new font hinting setting that will be applied at the start of the next frame
83+
#[cfg_attr(feature = "persistence", serde(skip))]
84+
pub(crate) new_font_hinting: Option<bool>,
85+
8286
/// add new font that will be applied at the start of the next frame
8387
#[cfg_attr(feature = "persistence", serde(skip))]
8488
pub(crate) add_fonts: Vec<epaint::text::FontInsert>,
@@ -125,6 +129,7 @@ impl Default for Memory {
125129
data: Default::default(),
126130
caches: Default::default(),
127131
new_font_definitions: Default::default(),
132+
new_font_hinting: Default::default(),
128133
interactions: Default::default(),
129134
focus: Default::default(),
130135
viewport_id: Default::default(),

crates/egui/src/style.rs

+14
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,7 @@ impl Widget for &mut FontTweak {
26262626
y_offset_factor,
26272627
y_offset,
26282628
baseline_offset_factor,
2629+
hinting_override,
26292630
} = self;
26302631

26312632
ui.label("Scale");
@@ -2645,6 +2646,19 @@ impl Widget for &mut FontTweak {
26452646
ui.add(DragValue::new(baseline_offset_factor).speed(-0.0025));
26462647
ui.end_row();
26472648

2649+
ui.label("hinting_override");
2650+
ComboBox::from_id_salt("hinting_override")
2651+
.selected_text(match hinting_override {
2652+
None => "None",
2653+
Some(true) => "Enable",
2654+
Some(false) => "Disable",
2655+
})
2656+
.show_ui(ui, |ui| {
2657+
ui.selectable_value(hinting_override, None, "None");
2658+
ui.selectable_value(hinting_override, Some(true), "Enable");
2659+
ui.selectable_value(hinting_override, Some(false), "Disable");
2660+
});
2661+
26482662
if ui.button("Reset").clicked() {
26492663
*self = Default::default();
26502664
}

crates/epaint/src/text/fonts.rs

+38
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ pub struct FontTweak {
103103
/// A positive value shifts the text downwards.
104104
/// A negative value shifts it upwards.
105105
pub baseline_offset_factor: f32,
106+
107+
/// Override the global font hinting setting for this specific font.
108+
///
109+
/// `None` means use the global setting.
110+
pub hinting_override: Option<bool>,
106111
}
107112

108113
impl Default for FontTweak {
@@ -112,6 +117,7 @@ impl Default for FontTweak {
112117
y_offset_factor: 0.0,
113118
y_offset: 0.0,
114119
baseline_offset_factor: 0.0,
120+
hinting_override: None,
115121
}
116122
}
117123
}
@@ -345,6 +351,7 @@ pub(super) struct FontsLayoutView<'a> {
345351
pub texture_atlas: &'a mut TextureAtlas,
346352
pub glyph_atlas: &'a mut GlyphAtlas,
347353
pub font_tweaks: &'a mut ahash::HashMap<u64, FontTweak>,
354+
pub hinting_enabled: bool,
348355
pub pixels_per_point: f32,
349356
}
350357

@@ -363,6 +370,7 @@ pub struct FontStore {
363370
max_texture_side: usize,
364371
definitions: FontDefinitions,
365372
font_tweaks: ahash::HashMap<u64, FontTweak>,
373+
hinting_enabled: bool,
366374
atlas: TextureAtlas,
367375
galley_cache: GalleyCache,
368376

@@ -399,6 +407,7 @@ impl FontStore {
399407
max_texture_side,
400408
definitions,
401409
font_tweaks: Default::default(),
410+
hinting_enabled: true,
402411
glyph_atlas: GlyphAtlas::new(),
403412
atlas,
404413
galley_cache: Default::default(),
@@ -500,6 +509,15 @@ impl FontStore {
500509
self.load_fonts_from_definitions();
501510
}
502511

512+
pub fn hinting_enabled(&self) -> bool {
513+
self.hinting_enabled
514+
}
515+
516+
pub fn set_hinting_enabled(&mut self, enabled: bool) {
517+
self.hinting_enabled = enabled;
518+
self.clear_cache(self.max_texture_side);
519+
}
520+
503521
/// Width of this character in points.
504522
pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
505523
*self.glyph_width_cache.entry(c).or_insert_with(|| {
@@ -559,6 +577,7 @@ impl FontStore {
559577
&mut self.atlas,
560578
&run,
561579
vec2(x_offset, 0.0),
580+
self.hinting_enabled,
562581
pixels_per_point,
563582
&self.font_tweaks,
564583
)
@@ -730,12 +749,19 @@ impl Fonts<'_> {
730749
self.fonts.definitions()
731750
}
732751

752+
#[inline]
753+
pub fn hinting_enabled(&self) -> bool {
754+
self.fonts.hinting_enabled()
755+
}
756+
733757
/// Width of this character in points.
758+
#[inline]
734759
pub fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 {
735760
self.fonts.glyph_width(font_id, c)
736761
}
737762

738763
/// Can we display all the glyphs in this text?
764+
#[inline]
739765
pub fn has_glyphs_for(&mut self, font_id: &FontId, s: &str) -> bool {
740766
self.fonts.has_glyphs_for(font_id, s)
741767
}
@@ -746,13 +772,15 @@ impl Fonts<'_> {
746772
/// Height of one row of text in points.
747773
///
748774
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
775+
#[inline]
749776
pub fn row_height(&mut self, font_id: &FontId) -> f32 {
750777
self.fonts
751778
.row_height(font_id)
752779
.round_to_pixels(self.pixels_per_point)
753780
}
754781

755782
/// Call at the end of each frame (before painting) to get the change to the font texture since last call.
783+
#[inline]
756784
pub fn font_image_delta(&mut self) -> Option<crate::ImageDelta> {
757785
self.fonts.font_image_delta()
758786
}
@@ -764,21 +792,25 @@ impl Fonts<'_> {
764792

765793
/// The font atlas.
766794
/// Pass this to [`crate::Tessellator`].
795+
#[inline]
767796
pub fn texture_atlas(&self) -> &TextureAtlas {
768797
self.fonts.texture_atlas()
769798
}
770799

771800
/// Current size of the font image.
772801
/// Pass this to [`crate::Tessellator`].
802+
#[inline]
773803
pub fn font_image_size(&self) -> [usize; 2] {
774804
self.fonts.font_image_size()
775805
}
776806

777807
/// List of all loaded font families.
808+
#[inline]
778809
pub fn families(&mut self) -> &[FontFamily] {
779810
self.fonts.families()
780811
}
781812

813+
#[inline]
782814
pub fn num_galleys_in_cache(&self) -> usize {
783815
self.fonts.num_galleys_in_cache()
784816
}
@@ -787,6 +819,7 @@ impl Fonts<'_> {
787819
///
788820
/// This increases as new fonts and/or glyphs are used,
789821
/// but can also decrease in a call to [`Self::begin_pass`].
822+
#[inline]
790823
pub fn font_atlas_fill_ratio(&self) -> f32 {
791824
self.fonts.font_atlas_fill_ratio()
792825
}
@@ -807,6 +840,7 @@ impl Fonts<'_> {
807840
texture_atlas: &mut self.fonts.atlas,
808841
glyph_atlas: &mut self.fonts.glyph_atlas,
809842
font_tweaks: &mut self.fonts.font_tweaks,
843+
hinting_enabled: self.fonts.hinting_enabled,
810844
pixels_per_point: self.pixels_per_point,
811845
},
812846
job,
@@ -827,6 +861,7 @@ impl Fonts<'_> {
827861
texture_atlas: &mut self.fonts.atlas,
828862
glyph_atlas: &mut self.fonts.glyph_atlas,
829863
font_tweaks: &mut self.fonts.font_tweaks,
864+
hinting_enabled: self.fonts.hinting_enabled,
830865
pixels_per_point: self.pixels_per_point,
831866
},
832867
job,
@@ -836,6 +871,7 @@ impl Fonts<'_> {
836871
/// Will wrap text at the given width and line break at `\n`.
837872
///
838873
/// The implementation uses memoization so repeated calls are cheap.
874+
#[inline]
839875
pub fn layout(
840876
&mut self,
841877
text: String,
@@ -850,6 +886,7 @@ impl Fonts<'_> {
850886
/// Will line break at `\n`.
851887
///
852888
/// The implementation uses memoization so repeated calls are cheap.
889+
#[inline]
853890
pub fn layout_no_wrap(
854891
&mut self,
855892
text: String,
@@ -863,6 +900,7 @@ impl Fonts<'_> {
863900
/// Like [`Self::layout`], made for when you want to pick a color for the text later.
864901
///
865902
/// The implementation uses memoization so repeated calls are cheap.
903+
#[inline]
866904
pub fn layout_delayed_color(
867905
&mut self,
868906
text: String,

crates/epaint/src/text/glyph_atlas.rs

+21-9
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,33 @@ struct StyleKey<'a> {
115115
font_id: u64,
116116
font_size: OrderedFloat<f32>,
117117
skew: i8,
118+
hinting_enabled: bool,
118119
/// We want to avoid doing a bunch of allocations. When looking up this key in a map, this can be a borrowed slice.
119120
/// We only need to convert it to an owned [`Vec<i16>`] the first time we insert it into the map.
120121
normalized_coords: Cow<'a, [i16]>,
121122
}
122123

123124
impl<'a> StyleKey<'a> {
124-
fn new(font_id: u64, font_size: f32, skew: i8, normalized_coords: &'a [i16]) -> Self {
125+
fn new(
126+
font_id: u64,
127+
font_size: f32,
128+
skew: i8,
129+
hinting_enabled: bool,
130+
normalized_coords: &'a [i16],
131+
) -> Self {
125132
Self {
126133
font_id,
127134
font_size: font_size.into(),
128135
skew,
136+
hinting_enabled,
129137
normalized_coords: Cow::Borrowed(normalized_coords),
130138
}
131139
}
132140

133141
fn to_static(&self) -> StyleKey<'static> {
134142
StyleKey {
135-
font_id: self.font_id,
136-
font_size: self.font_size,
137-
skew: self.skew,
138143
normalized_coords: self.normalized_coords.clone().into_owned().into(),
144+
..*self
139145
}
140146
}
141147
}
@@ -184,6 +190,7 @@ impl GlyphAtlas {
184190
atlas: &'c mut TextureAtlas,
185191
glyph_run: &'b GlyphRun<'b, Color32>,
186192
offset: Vec2,
193+
hinting_enabled: bool,
187194
pixels_per_point: f32,
188195
font_tweaks: &ahash::HashMap<u64, FontTweak>,
189196
) -> impl Iterator<Item = (Glyph, Option<UvRect>, (i32, i32), Color32)> + use<'a, 'b, 'c> {
@@ -209,12 +216,20 @@ impl GlyphAtlas {
209216
let size = font_size * pixels_per_point;
210217
let normalized_coords = run.normalized_coords();
211218

219+
let font_tweak = font_tweaks.get(&font_id);
220+
let tweak_offset = font_tweak.map_or(0.0, |tweak| {
221+
(font_size * tweak.y_offset_factor) + tweak.y_offset
222+
});
223+
let hinting_enabled = font_tweak
224+
.and_then(|tweak| tweak.hinting_override)
225+
.unwrap_or(hinting_enabled);
226+
212227
let mut scaler: swash::scale::Scaler<'b> = self
213228
.scale_context
214229
.builder(font_ref)
215230
.size(size)
216231
.normalized_coords(normalized_coords)
217-
.hint(true)
232+
.hint(hinting_enabled)
218233
.build();
219234
let rendered_glyphs = &mut self.rendered_glyphs;
220235
let color = glyph_run.style().brush;
@@ -227,6 +242,7 @@ impl GlyphAtlas {
227242
font_id,
228243
size,
229244
skew.unwrap_or_default() as i8,
245+
hinting_enabled,
230246
normalized_coords,
231247
);
232248

@@ -242,10 +258,6 @@ impl GlyphAtlas {
242258
}),
243259
};
244260

245-
let tweak_offset = font_tweaks.get(&font_id).map_or(0.0, |tweak| {
246-
(font_size * tweak.y_offset_factor) + tweak.y_offset
247-
});
248-
249261
glyph_run.positioned_glyphs().map(move |mut glyph| {
250262
// The Y-position transform applies to the font *after* it's been hinted, making it blurry. (So does the
251263
// X-position transform, but the hinter doesn't change the X coordinates anymore.)

0 commit comments

Comments
 (0)