1
0
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:
Emil Ernerfeldt
2026-06-24 20:55:57 +02:00
committed by GitHub
parent 600286d056
commit 7c600b3c76
4 changed files with 176 additions and 122 deletions

View File

@@ -3206,7 +3206,8 @@ impl Context {
for (name, data) in &mut font_definitions.font_data {
ui.collapsing(name, |ui| {
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;
changed = true;
}

View File

@@ -3,8 +3,7 @@
use emath::Align;
use epaint::{
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
mutex::Mutex,
text::{FontTweak, Tag},
text::{FontTweak, FontVariationAxis},
};
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 {
fn ui(self, ui: &mut Ui) -> Response {
let original: FontTweak = self.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,
} = 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
font_tweak_ui(ui, self, &[])
}
}