From 3abba21f2db984294f912768f496e3b175f2a984 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 10:38:37 +0200 Subject: [PATCH] Add `subpixel_binning` to `TextOptions` and `FontTweak` (#8072) This lets you turn off subpixel horizontal binning of glyphs. The option is a trade-off between even kerning and sharp text. * Closes https://github.com/emilk/egui/issues/8034 --- crates/egui/src/style.rs | 33 +++++++++++++++++++-------------- crates/epaint/src/text/font.rs | 12 ++++++++---- crates/epaint/src/text/fonts.rs | 12 +++++++++--- crates/epaint/src/text/mod.rs | 15 +++++++++++++++ 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index b84fe727d..c88ee45fe 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2344,11 +2344,13 @@ impl Visuals { max_texture_side: _, alpha_from_coverage, font_hinting, + subpixel_binning, } = text_options; text_alpha_from_coverage_ui(ui, alpha_from_coverage); - ui.checkbox(font_hinting, "Enable font hinting"); + ui.checkbox(font_hinting, "Font hinting (sharper text)"); + ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)"); }); ui.collapsing("Text cursor", |ui| { @@ -2913,10 +2915,11 @@ impl Widget for &mut FontTweak { scale, y_offset_factor, y_offset, - hinting_override, + hinting, coords, thin_space_width, tab_size, + subpixel_binning, } = self; ui.label("Scale"); @@ -2932,18 +2935,20 @@ 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"); - }); + ui.label("hinting"); + ui.horizontal(|ui| { + ui.radio_value(hinting, Some(true), "on"); + ui.radio_value(hinting, Some(false), "off"); + ui.radio_value(hinting, None, "default"); + }); + ui.end_row(); + + ui.label("subpixel_binning"); + ui.horizontal(|ui| { + ui.radio_value(subpixel_binning, Some(true), "on"); + ui.radio_value(subpixel_binning, Some(false), "off"); + ui.radio_value(subpixel_binning, None, "default"); + }); ui.end_row(); ui.label("coords"); diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 311b17a05..2d5edf1cf 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -316,6 +316,7 @@ pub struct FontFace { name: String, font: FontCell, tweak: FontTweak, + subpixel_binning: bool, /// Cached `harfrust` shaper data (parsed GSUB/GPOS tables). /// `ShaperData` is `Copy` — lives outside the `self_cell`. @@ -352,7 +353,7 @@ impl FontFace { skrifa::instance::LocationRef::default(), ); - let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting); + let hinting_enabled = tweak.hinting.unwrap_or(options.font_hinting); let hinting_instance = hinting_enabled .then(|| { // It doesn't really matter what we put here for options. Since the size is `unscaled()`, we will @@ -379,10 +380,13 @@ impl FontFace { let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa); + let subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning); + Ok(Self { name, font, tweak, + subpixel_binning, shaper_data, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), @@ -551,12 +555,12 @@ impl FontFace { return (GlyphAllocation::default(), h_pos.round() as i32); } - let (h_pos_round, bin) = if is_cjk { + let (h_pos_round, bin) = if self.subpixel_binning && !is_cjk { + SubpixelBin::new(h_pos) + } else { // CJK scripts contain a lot of characters and could hog the glyph atlas // if we stored 4 subpixel offsets per glyph. (h_pos.round() as i32, SubpixelBin::Zero) - } else { - SubpixelBin::new(h_pos) }; let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin); diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b6c8f3504..f1a6e63b4 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -188,8 +188,13 @@ pub struct FontTweak { /// Override the global font hinting setting for this specific font. /// - /// `None` means use the global setting. - pub hinting_override: Option, + /// `None` means use the global setting in [`TextOptions::font_hinting`]. + pub hinting: Option, + + /// Override the global sub-pixel binning setting for this specific font. + /// + /// `None` means use the global setting in [`TextOptions::subpixel_binning`]. + pub subpixel_binning: Option, /// Override the font's default variation coordinates. pub coords: VariationCoords, @@ -214,7 +219,8 @@ impl Default for FontTweak { scale: 1.0, y_offset_factor: 0.0, y_offset: 0.0, - hinting_override: None, + hinting: None, + subpixel_binning: None, coords: VariationCoords::default(), thin_space_width: 0.5, tab_size: 4.0, diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index 7d37c0db6..6d2d783c2 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -34,6 +34,20 @@ pub struct TextOptions { /// /// Default is `true`. pub font_hinting: bool, + + /// Enable sub-pixel binning for glyphs. + /// + /// Sub-pixel binning renders each glyph at up to four fractional horizontal offsets, + /// giving more even kerning at the cost of more atlas space. + /// + /// It also lead to text looking more blurry. + /// + /// This is always disabled for CJK characters (which have too many unique glyphs). + /// + /// Can be overridden per font with [`FontTweak::subpixel_binning`]. + /// + /// Default: `true`. + pub subpixel_binning: bool, } impl Default for TextOptions { @@ -42,6 +56,7 @@ impl Default for TextOptions { max_texture_side: 2048, // Small but portable alpha_from_coverage: crate::AlphaFromCoverage::default(), font_hinting: true, + subpixel_binning: true, } } }