mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Make font hinting target configurable via FontTweak (#8262)
Fixes #8079, where font hinting only sharpens the vertical axis (vertical stems stay blurry), and none of the skrifa knobs were reachable — only switching to `Target::Mono` helped, but that wasn't exposed. This makes the hinting target configurable instead of hardcoding `Target::Smooth { symmetric_rendering: true, preserve_linear_metrics: true }`. ### API - New `epaint::text::HintingTarget` mirroring `skrifa::outline::Target`: - `Mono` - `Smooth(SmoothHinting)` where `SmoothHinting { light, symmetric_rendering, preserve_linear_metrics }` - New field `FontTweak::hinting_target: HintingTarget`. - Each variant/field is documented with what it does and the egui-specific caveats (e.g. `symmetric_rendering` only affects interpreter-hinted fonts; egui positions glyphs from shaper advances so `preserve_linear_metrics` mostly affects sharpness, not layout). ### Render - `font.rs` converts `HintingTarget` → `skrifa::outline::Target` and threads it into the per-glyph `reconfigure` call; the hinting instance is also reconfigured when the target changes. ### UI - The font-tweak settings panel gets a `hinting_target` row: Smooth/Mono radios, `light` / `symmetric_rendering` / `preserve_linear_metrics` checkboxes, and a `Reset` button — all with tooltips. ### Behavior - `HintingTarget::default()` matches egui's previous hardcoded target, so **rendering is unchanged** unless you opt in. To fix the horizontal blur from #8079, uncheck `preserve_linear_metrics` (or pick `Mono`). Whether to flip the *default* is left as a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{
|
use epaint::{
|
||||||
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
||||||
text::{FontTweak, FontVariationAxis},
|
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
|
||||||
};
|
};
|
||||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||||
|
|
||||||
@@ -2902,6 +2902,7 @@ pub fn font_tweak_ui(ui: &mut Ui, tweak: &mut FontTweak, axes: &[FontVariationAx
|
|||||||
y_offset_factor,
|
y_offset_factor,
|
||||||
y_offset,
|
y_offset,
|
||||||
hinting,
|
hinting,
|
||||||
|
hinting_target,
|
||||||
coords,
|
coords,
|
||||||
thin_space_width,
|
thin_space_width,
|
||||||
tab_size,
|
tab_size,
|
||||||
@@ -2929,6 +2930,59 @@ pub fn font_tweak_ui(ui: &mut Ui, tweak: &mut FontTweak, axes: &[FontVariationAx
|
|||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("hinting_target")
|
||||||
|
.on_hover_text("How aggressively to snap glyph outlines to the pixel grid. Only matters when hinting is enabled.");
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let is_mono = matches!(hinting_target, HintingTarget::Mono);
|
||||||
|
if ui
|
||||||
|
.radio(!is_mono, "Smooth")
|
||||||
|
.on_hover_text("Hinting tuned for anti-aliased rendering. The normal choice.")
|
||||||
|
.clicked()
|
||||||
|
&& is_mono
|
||||||
|
{
|
||||||
|
*hinting_target = HintingTarget::default();
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.radio(is_mono, "Mono")
|
||||||
|
.on_hover_text(
|
||||||
|
"Strongest hinting (designed for 1-bit rendering). Sharpest, but \
|
||||||
|
distorts glyph weight across sizes.",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*hinting_target = HintingTarget::Mono;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button("Reset")
|
||||||
|
.on_hover_text("Reset the hinting target to its default.")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*hinting_target = HintingTarget::default();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let HintingTarget::Smooth(SmoothHinting {
|
||||||
|
light,
|
||||||
|
symmetric_rendering,
|
||||||
|
preserve_linear_metrics,
|
||||||
|
}) = hinting_target
|
||||||
|
{
|
||||||
|
ui.checkbox(light, "light").on_hover_text(
|
||||||
|
"Hint only vertically, preserving the font's horizontal proportions \
|
||||||
|
(softer). Off also fits horizontally.",
|
||||||
|
);
|
||||||
|
ui.checkbox(symmetric_rendering, "symmetric_rendering").on_hover_text(
|
||||||
|
"Render glyphs the same regardless of sub-pixel position (good for \
|
||||||
|
caching/animation), but can blur stems. Only affects interpreter-hinted fonts.",
|
||||||
|
);
|
||||||
|
ui.checkbox(preserve_linear_metrics, "preserve_linear_metrics").on_hover_text(
|
||||||
|
"Keep spacing independent of hinting. Off lets the hinter snap \
|
||||||
|
horizontally for crisper vertical stems on low-dpi screens.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("subpixel_binning");
|
ui.label("subpixel_binning");
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.radio_value(subpixel_binning, Some(true), "on");
|
ui.radio_value(subpixel_binning, Some(true), "on");
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ impl FontCell {
|
|||||||
glyph_id: GlyphId,
|
glyph_id: GlyphId,
|
||||||
bin: SubpixelBin,
|
bin: SubpixelBin,
|
||||||
location: skrifa::instance::LocationRef<'_>,
|
location: skrifa::instance::LocationRef<'_>,
|
||||||
|
hinting_target: skrifa::outline::Target,
|
||||||
) -> Option<GlyphAllocation> {
|
) -> Option<GlyphAllocation> {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
glyph_id != skrifa::GlyphId::NOTDEF,
|
glyph_id != skrifa::GlyphId::NOTDEF,
|
||||||
@@ -244,18 +245,10 @@ impl FontCell {
|
|||||||
let size = skrifa::instance::Size::new(metrics.scale);
|
let size = skrifa::instance::Size::new(metrics.scale);
|
||||||
if hinting_instance.size() != size
|
if hinting_instance.size() != size
|
||||||
|| hinting_instance.location().coords() != location.coords()
|
|| hinting_instance.location().coords() != location.coords()
|
||||||
|
|| hinting_instance.target() != hinting_target
|
||||||
{
|
{
|
||||||
hinting_instance
|
hinting_instance
|
||||||
.reconfigure(
|
.reconfigure(&font_data.outline_glyphs, size, location, hinting_target)
|
||||||
&font_data.outline_glyphs,
|
|
||||||
size,
|
|
||||||
location,
|
|
||||||
skrifa::outline::Target::Smooth {
|
|
||||||
mode: skrifa::outline::SmoothMode::Normal,
|
|
||||||
symmetric_rendering: true,
|
|
||||||
preserve_linear_metrics: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.ok()?;
|
.ok()?;
|
||||||
}
|
}
|
||||||
let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
|
let draw_settings = skrifa::outline::DrawSettings::hinted(hinting_instance, false);
|
||||||
@@ -637,9 +630,17 @@ impl FontFace {
|
|||||||
|
|
||||||
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
|
let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin);
|
||||||
|
|
||||||
|
let hinting_target = self.tweak.hinting_target.into();
|
||||||
let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| {
|
let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| {
|
||||||
self.font
|
self.font
|
||||||
.allocate_glyph_uncached(atlas, metrics, glyph_id, bin, (&metrics.location).into())
|
.allocate_glyph_uncached(
|
||||||
|
atlas,
|
||||||
|
metrics,
|
||||||
|
glyph_id,
|
||||||
|
bin,
|
||||||
|
(&metrics.location).into(),
|
||||||
|
hinting_target,
|
||||||
|
)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,11 @@ pub struct FontTweak {
|
|||||||
/// `None` means use the global setting in [`TextOptions::font_hinting`].
|
/// `None` means use the global setting in [`TextOptions::font_hinting`].
|
||||||
pub hinting: Option<bool>,
|
pub hinting: Option<bool>,
|
||||||
|
|
||||||
|
/// How to grid-fit the glyph outlines when hinting is enabled.
|
||||||
|
///
|
||||||
|
/// Has no effect when hinting is disabled (see [`Self::hinting`]).
|
||||||
|
pub hinting_target: HintingTarget,
|
||||||
|
|
||||||
/// Override the global sub-pixel binning setting for this specific font.
|
/// Override the global sub-pixel binning setting for this specific font.
|
||||||
///
|
///
|
||||||
/// `None` means use the global setting in [`TextOptions::subpixel_binning`].
|
/// `None` means use the global setting in [`TextOptions::subpixel_binning`].
|
||||||
@@ -271,6 +276,7 @@ impl Default for FontTweak {
|
|||||||
y_offset_factor: 0.0,
|
y_offset_factor: 0.0,
|
||||||
y_offset: 0.0,
|
y_offset: 0.0,
|
||||||
hinting: None,
|
hinting: None,
|
||||||
|
hinting_target: HintingTarget::default(),
|
||||||
subpixel_binning: None,
|
subpixel_binning: None,
|
||||||
coords: VariationCoords::default(),
|
coords: VariationCoords::default(),
|
||||||
thin_space_width: 0.5,
|
thin_space_width: 0.5,
|
||||||
@@ -281,6 +287,111 @@ impl Default for FontTweak {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// How to *hint* glyph outlines, i.e. how aggressively to nudge them onto the
|
||||||
|
/// pixel grid before rasterizing. Mirrors [`skrifa::outline::Target`].
|
||||||
|
///
|
||||||
|
/// Hinting trades shape fidelity for sharpness: snapping stems to whole pixels
|
||||||
|
/// makes text crisp at small sizes / low dpi, at the cost of slightly distorting
|
||||||
|
/// the designer's outlines. At high dpi it matters little.
|
||||||
|
///
|
||||||
|
/// This only has an effect if the font is actually hinted — either it ships
|
||||||
|
/// TrueType instructions, or it was auto-hinted (see [`FontTweak::hinting`]).
|
||||||
|
///
|
||||||
|
/// Used by [`FontTweak::hinting_target`].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub enum HintingTarget {
|
||||||
|
/// Strongest hinting, designed for aliased 1-bit (black & white) rendering.
|
||||||
|
///
|
||||||
|
/// Snaps stems hard to the pixel grid for maximum sharpness. egui always
|
||||||
|
/// renders anti-aliased, so in practice this looks much like [`Self::Smooth`]
|
||||||
|
/// here; it mostly exists for completeness. Maps to `skrifa`'s `Target::Mono`.
|
||||||
|
Mono,
|
||||||
|
|
||||||
|
/// Hinting tuned for anti-aliased rendering. This is what you normally want,
|
||||||
|
/// and what egui uses by default. Maps to `skrifa`'s `Target::Smooth`.
|
||||||
|
Smooth(SmoothHinting),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HintingTarget {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Smooth(SmoothHinting::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HintingTarget> for skrifa::outline::Target {
|
||||||
|
fn from(hinting_target: HintingTarget) -> Self {
|
||||||
|
use skrifa::outline::SmoothMode;
|
||||||
|
match hinting_target {
|
||||||
|
HintingTarget::Mono => Self::Mono,
|
||||||
|
HintingTarget::Smooth(SmoothHinting {
|
||||||
|
light,
|
||||||
|
symmetric_rendering,
|
||||||
|
preserve_linear_metrics,
|
||||||
|
}) => Self::Smooth {
|
||||||
|
mode: if light {
|
||||||
|
SmoothMode::Light
|
||||||
|
} else {
|
||||||
|
SmoothMode::Normal
|
||||||
|
},
|
||||||
|
symmetric_rendering,
|
||||||
|
preserve_linear_metrics,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tuning for [`HintingTarget::Smooth`], mirroring `skrifa`'s `Target::Smooth`.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct SmoothHinting {
|
||||||
|
/// Hint only lightly: snap stems vertically but leave horizontal shapes
|
||||||
|
/// alone (`FreeType`'s "light" mode). Preserves the font's proportions
|
||||||
|
/// better, at the cost of a little horizontal sharpness.
|
||||||
|
///
|
||||||
|
/// `false` uses the "normal" mode, which also fits horizontally.
|
||||||
|
/// Maps to `SmoothMode::Light` (`true`) vs `SmoothMode::Normal` (`false`).
|
||||||
|
pub light: bool,
|
||||||
|
|
||||||
|
/// Render a glyph the same way regardless of its sub-pixel position.
|
||||||
|
///
|
||||||
|
/// `true` makes glyphs position-independent (better for caching and
|
||||||
|
/// animation), but a font's instructions may then widen stems and look
|
||||||
|
/// slightly blurrier under an analytic rasterizer like egui's.
|
||||||
|
///
|
||||||
|
/// **Only affects fonts hinted via the TrueType interpreter** (i.e. fonts
|
||||||
|
/// that ship their own instructions). It has no effect on the auto-hinter.
|
||||||
|
/// Mirrors `Target::Smooth { symmetric_rendering }`.
|
||||||
|
pub symmetric_rendering: bool,
|
||||||
|
|
||||||
|
/// Keep advance widths independent of hinting (don't grid-fit horizontally
|
||||||
|
/// in a way that changes spacing).
|
||||||
|
///
|
||||||
|
/// `true` keeps inter-glyph spacing identical to the unhinted font, so
|
||||||
|
/// layout never depends on hinting — but it also prevents horizontal
|
||||||
|
/// grid-fitting, leaving vertical stems softer on low-dpi screens.
|
||||||
|
///
|
||||||
|
/// `false` lets the (auto)hinter snap horizontally for crisper stems.
|
||||||
|
/// egui positions glyphs from the shaper's advances, not the hinted
|
||||||
|
/// outline, so this mainly affects sharpness here, not layout.
|
||||||
|
/// Mirrors `Target::Smooth { preserve_linear_metrics }`.
|
||||||
|
pub preserve_linear_metrics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SmoothHinting {
|
||||||
|
fn default() -> Self {
|
||||||
|
// Matches the behavior egui had before the hinting target was configurable.
|
||||||
|
// Note this means horizontal grid-fitting is opt-in (see `preserve_linear_metrics`).
|
||||||
|
Self {
|
||||||
|
light: false,
|
||||||
|
symmetric_rendering: true,
|
||||||
|
preserve_linear_metrics: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
pub type Blob = Arc<dyn AsRef<[u8]> + Send + Sync>;
|
pub type Blob = Arc<dyn AsRef<[u8]> + Send + Sync>;
|
||||||
|
|
||||||
fn blob_from_font_data(data: &FontData) -> Blob {
|
fn blob_from_font_data(data: &FontData) -> Blob {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ mod text_layout_types;
|
|||||||
pub use {
|
pub use {
|
||||||
fonts::{
|
fonts::{
|
||||||
FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak,
|
FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak,
|
||||||
FontVariationAxis, Fonts, FontsImpl, FontsView, InsertFontFamily,
|
FontVariationAxis, Fonts, FontsImpl, FontsView, HintingTarget, InsertFontFamily,
|
||||||
|
SmoothHinting,
|
||||||
},
|
},
|
||||||
index::{ByteIndex, ByteRange, ByteRangeExt, CharIndex, CharRange, CharRangeExt},
|
index::{ByteIndex, ByteRange, ByteRangeExt, CharIndex, CharRange, CharRangeExt},
|
||||||
text_layout::*,
|
text_layout::*,
|
||||||
|
|||||||
Reference in New Issue
Block a user