mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Pre-populate font variation axes in the FontTweak UI (#8258)
The `FontTweak` settings UI previously let you edit variable-font variation coordinates only via free-form tag + value entry — you had to *know* that e.g. `wght` exists and what range is valid. This PR queries the font's actual variation axes and pre-populates the UI. ### Changes - **`epaint`**: new `FontData::variation_axes() -> Vec<FontVariationAxis>` (skrifa-backed). Each `FontVariationAxis` exposes the axis `tag`, human-readable `name`, `min`/`default`/`max`, and `hidden`. Empty for static (non-variable) fonts. - **`egui`**: extracted the `FontTweak` body into a public `style::font_tweak_ui(ui, tweak, axes)`. When `axes` is non-empty, each axis is shown as a named **slider** pre-filled with the font's default and clamped to its valid range, with a ⟲ button to drop the override. `impl Widget for &mut FontTweak` still exists and delegates with no axes (free-form fallback, also used for unknown/manual tags). - The font settings panel (`Context::fonts_tweak_ui`) now passes `data.variation_axes()`. - UI label renamed `coords` → **Axes** (matching Google Fonts' terminology); the underlying `FontTweak.coords` field keeps the OpenType "design coordinates" name. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Example (Weight and Width): <img width="340" height="239" alt="Screenshot 2026-06-24 at 11 51 33" src="https://github.com/user-attachments/assets/f898289a-e329-453a-ba86-c60858901466" /> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3206,7 +3206,8 @@ impl Context {
|
|||||||
for (name, data) in &mut font_definitions.font_data {
|
for (name, data) in &mut font_definitions.font_data {
|
||||||
ui.collapsing(name, |ui| {
|
ui.collapsing(name, |ui| {
|
||||||
let mut tweak = data.tweak.clone();
|
let mut tweak = data.tweak.clone();
|
||||||
if tweak.ui(ui).changed() {
|
let axes = data.variation_axes();
|
||||||
|
if crate::style::font_tweak_ui(ui, &mut tweak, &axes).changed() {
|
||||||
Arc::make_mut(data).tweak = tweak;
|
Arc::make_mut(data).tweak = tweak;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{
|
use epaint::{
|
||||||
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
||||||
mutex::Mutex,
|
text::{FontTweak, FontVariationAxis},
|
||||||
text::{FontTweak, Tag},
|
|
||||||
};
|
};
|
||||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||||
|
|
||||||
@@ -2882,121 +2881,124 @@ impl Widget for &mut crate::Frame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show a UI for editing a [`FontTweak`].
|
||||||
|
///
|
||||||
|
/// `axes` are the variation axes of the font this tweak applies to, as returned by
|
||||||
|
/// [`epaint::text::FontData::variation_axes`]. When non-empty, the variation
|
||||||
|
/// coordinates are shown as named sliders pre-populated with each axis' valid range
|
||||||
|
/// and default value, so the user doesn't have to guess tags and numbers. Pass an
|
||||||
|
/// empty slice if the axes are unknown (e.g. a static font) to fall back to
|
||||||
|
/// free-form tag/value entry.
|
||||||
|
///
|
||||||
|
/// [`Widget for &mut FontTweak`](FontTweak) calls this with no axes.
|
||||||
|
pub fn font_tweak_ui(ui: &mut Ui, tweak: &mut FontTweak, axes: &[FontVariationAxis]) -> Response {
|
||||||
|
let original: FontTweak = tweak.clone();
|
||||||
|
|
||||||
|
let mut response = Grid::new("font_tweak")
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let FontTweak {
|
||||||
|
scale,
|
||||||
|
y_offset_factor,
|
||||||
|
y_offset,
|
||||||
|
hinting,
|
||||||
|
coords,
|
||||||
|
thin_space_width,
|
||||||
|
tab_size,
|
||||||
|
subpixel_binning,
|
||||||
|
} = tweak;
|
||||||
|
|
||||||
|
ui.label("Scale");
|
||||||
|
let speed = *scale * 0.01;
|
||||||
|
ui.add(DragValue::new(scale).range(0.01..=10.0).speed(speed));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("y_offset_factor");
|
||||||
|
ui.add(DragValue::new(y_offset_factor).speed(-0.0025));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("y_offset");
|
||||||
|
ui.add(DragValue::new(y_offset).speed(-0.02));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
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("thin_space_width");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(
|
||||||
|
DragValue::new(thin_space_width)
|
||||||
|
.range(0.0..=1.0)
|
||||||
|
.speed(0.01),
|
||||||
|
);
|
||||||
|
ui.label("1\u{2009}234\u{2009}567\u{2009}890");
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("tab_size");
|
||||||
|
ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
// Show variation axes if we have them:
|
||||||
|
for axis in axes.iter().filter(|axis| !axis.hidden) {
|
||||||
|
match &axis.name {
|
||||||
|
Some(name) => ui.label(format!("{name} ({})", axis.tag)),
|
||||||
|
None => ui.label(axis.tag.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag);
|
||||||
|
let mut value = existing.map_or(axis.default, |i| coords.as_ref()[i].1);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.add(Slider::new(&mut value, axis.range.into())).changed() {
|
||||||
|
match existing {
|
||||||
|
Some(i) => coords.as_mut()[i].1 = value,
|
||||||
|
None => coords.push(axis.tag, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Let the user drop the override and fall back to the font default:
|
||||||
|
if existing.is_some()
|
||||||
|
&& ui
|
||||||
|
.small_button("⟲")
|
||||||
|
.on_hover_text("Reset to the font's default value")
|
||||||
|
.clicked()
|
||||||
|
&& let Some(i) =
|
||||||
|
coords.as_ref().iter().position(|(tag, _)| *tag == axis.tag)
|
||||||
|
{
|
||||||
|
coords.remove(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Reset").clicked() {
|
||||||
|
*tweak = Default::default();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.response;
|
||||||
|
|
||||||
|
if *tweak != original {
|
||||||
|
response.mark_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
impl Widget for &mut FontTweak {
|
impl Widget for &mut FontTweak {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let original: FontTweak = self.clone();
|
font_tweak_ui(ui, self, &[])
|
||||||
|
|
||||||
let mut response = Grid::new("font_tweak")
|
|
||||||
.num_columns(2)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let FontTweak {
|
|
||||||
scale,
|
|
||||||
y_offset_factor,
|
|
||||||
y_offset,
|
|
||||||
hinting,
|
|
||||||
coords,
|
|
||||||
thin_space_width,
|
|
||||||
tab_size,
|
|
||||||
subpixel_binning,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
ui.label("Scale");
|
|
||||||
let speed = *scale * 0.01;
|
|
||||||
ui.add(DragValue::new(scale).range(0.01..=10.0).speed(speed));
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("y_offset_factor");
|
|
||||||
ui.add(DragValue::new(y_offset_factor).speed(-0.0025));
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("y_offset");
|
|
||||||
ui.add(DragValue::new(y_offset).speed(-0.02));
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
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");
|
|
||||||
ui.end_row();
|
|
||||||
let mut to_remove = None;
|
|
||||||
for (i, (tag, value)) in coords.as_mut().iter_mut().enumerate() {
|
|
||||||
let tag_text = ui.ctx().data_mut(|data| {
|
|
||||||
let tag = *tag;
|
|
||||||
Arc::clone(data.get_temp_mut_or_insert_with(ui.id().with(i), move || {
|
|
||||||
Arc::new(Mutex::new(tag.to_string()))
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
let tag_text = &mut *tag_text.lock();
|
|
||||||
let response = ui.text_edit_singleline(tag_text);
|
|
||||||
if response.changed()
|
|
||||||
&& let Ok(new_tag) = Tag::new_checked(tag_text.as_bytes())
|
|
||||||
{
|
|
||||||
*tag = new_tag;
|
|
||||||
}
|
|
||||||
// Reset stale text when not actively editing
|
|
||||||
// (e.g. after an item was removed and indices shifted)
|
|
||||||
if !response.has_focus()
|
|
||||||
&& Tag::new_checked(tag_text.as_bytes()).ok() != Some(*tag)
|
|
||||||
{
|
|
||||||
*tag_text = tag.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add(DragValue::new(value));
|
|
||||||
if ui.small_button("🗑").clicked() {
|
|
||||||
to_remove = Some(i);
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
}
|
|
||||||
if let Some(i) = to_remove {
|
|
||||||
coords.remove(i);
|
|
||||||
}
|
|
||||||
if ui.button("Add coord").clicked() {
|
|
||||||
coords.push(b"wght", 0.0);
|
|
||||||
}
|
|
||||||
if ui.button("Clear coords").clicked() {
|
|
||||||
coords.clear();
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("thin_space_width");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.add(
|
|
||||||
DragValue::new(thin_space_width)
|
|
||||||
.range(0.0..=1.0)
|
|
||||||
.speed(0.01),
|
|
||||||
);
|
|
||||||
ui.label("1\u{2009}234\u{2009}567\u{2009}890");
|
|
||||||
});
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("tab_size");
|
|
||||||
ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
if ui.button("Reset").clicked() {
|
|
||||||
*self = Default::default();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.response;
|
|
||||||
|
|
||||||
if *self != original {
|
|
||||||
response.mark_changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use std::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
TextureAtlas,
|
TextureAtlas,
|
||||||
text::{
|
text::{
|
||||||
ByteIndex, Galley, LayoutJob, LayoutSection, TextOptions, VariationCoords,
|
ByteIndex, Galley, LayoutJob, LayoutSection, Tag, TextOptions, VariationCoords,
|
||||||
font::{Font, FontFace},
|
font::{Font, FontFace},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use emath::{NumExt as _, OrderedFloat};
|
use emath::{NumExt as _, OrderedFloat, Rangef};
|
||||||
|
|
||||||
#[cfg(feature = "default_fonts")]
|
#[cfg(feature = "default_fonts")]
|
||||||
use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT};
|
use epaint_default_fonts::{EMOJI_ICON, HACK_REGULAR, NOTO_EMOJI_REGULAR, UBUNTU_LIGHT};
|
||||||
@@ -147,6 +147,57 @@ impl FontData {
|
|||||||
pub fn tweak(self, tweak: FontTweak) -> Self {
|
pub fn tweak(self, tweak: FontTweak) -> Self {
|
||||||
Self { tweak, ..self }
|
Self { tweak, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The variation axes of this font, e.g. `wght` (weight) and `wdth` (width).
|
||||||
|
///
|
||||||
|
/// Use this to discover which axes a variable font supports, and their valid
|
||||||
|
/// ranges, so a UI can offer the right knobs instead of making the user guess
|
||||||
|
/// tags and values for [`FontTweak::coords`].
|
||||||
|
///
|
||||||
|
/// Returns an empty list for non-variable (static) fonts, or if the font data
|
||||||
|
/// fails to parse.
|
||||||
|
pub fn variation_axes(&self) -> Vec<FontVariationAxis> {
|
||||||
|
use skrifa::MetadataProvider as _;
|
||||||
|
|
||||||
|
let Ok(font) = skrifa::FontRef::from_index(self.font.as_ref(), self.index) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
font.axes()
|
||||||
|
.iter()
|
||||||
|
.map(|axis| FontVariationAxis {
|
||||||
|
tag: axis.tag(),
|
||||||
|
name: font
|
||||||
|
.localized_strings(axis.name_id())
|
||||||
|
.english_or_first()
|
||||||
|
.map(|name| name.chars().collect()),
|
||||||
|
range: Rangef::new(axis.min_value(), axis.max_value()),
|
||||||
|
default: axis.default_value(),
|
||||||
|
hidden: axis.is_hidden(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single variation axis of a variable font, e.g. weight (`wght`) or width (`wdth`).
|
||||||
|
///
|
||||||
|
/// Obtained via [`FontData::variation_axes`].
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct FontVariationAxis {
|
||||||
|
/// The axis tag, e.g. `wght` or `wdth`.
|
||||||
|
pub tag: Tag,
|
||||||
|
|
||||||
|
/// Human-readable axis name, if the font provides one (e.g. "Weight").
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// Valid range of values for this axis, `min..=max`.
|
||||||
|
pub range: Rangef,
|
||||||
|
|
||||||
|
/// The value used when the axis is not overridden.
|
||||||
|
pub default: f32,
|
||||||
|
|
||||||
|
/// Whether the font recommends hiding this axis from user interfaces.
|
||||||
|
pub hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<[u8]> for FontData {
|
impl AsRef<[u8]> for FontData {
|
||||||
@@ -196,7 +247,7 @@ pub struct FontTweak {
|
|||||||
/// `None` means use the global setting in [`TextOptions::subpixel_binning`].
|
/// `None` means use the global setting in [`TextOptions::subpixel_binning`].
|
||||||
pub subpixel_binning: Option<bool>,
|
pub subpixel_binning: Option<bool>,
|
||||||
|
|
||||||
/// Override the font's default variation coordinates.
|
/// Override the font's default variation coordinates for its axes ("wght", etc.).
|
||||||
pub coords: VariationCoords,
|
pub coords: VariationCoords,
|
||||||
|
|
||||||
/// Width of a thin space (`\u{2009}`) and narrow no-break space (`\u{202F}`),
|
/// Width of a thin space (`\u{2009}`) and narrow no-break space (`\u{202F}`),
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ mod text_layout_types;
|
|||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
fonts::{
|
fonts::{
|
||||||
FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts,
|
FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak,
|
||||||
FontsImpl, FontsView, InsertFontFamily,
|
FontVariationAxis, Fonts, FontsImpl, FontsView, InsertFontFamily,
|
||||||
},
|
},
|
||||||
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