M13.6: variable font axes in the character style
CharStyle/TextDoc variations (layer.setText variations), shaped with harfrust instances so advances follow HVAR/gvar, outlines drawn at the design-space position, animator deltas relative to the style's values, Character panel Variable Font Axes fields (character.axis.<tag>). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7b0a449c5f
commit
f89af341a6
|
|
@ -1163,7 +1163,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Edit Text",
|
||||
[],
|
||||
None,
|
||||
"{layer?, range?: [start, end] (characters; default all), text?, font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?: px|\"auto\", baselineShift? px, hScale? %, vScale? %, tsume? %, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?: normal|superscript|subscript, superscript?, subscript?, kerning?: metrics|optical|number, ligatures?, OpenType: discretionaryLigatures?, contextualAlternates?, stylisticAlternates?, stylisticSets?: [1–20], ss01…ss20?, swash?, titling?, ordinals?, fractions?, allSmallCaps?, figureStyle?: default|lining|oldStyle, figureWidth?: default|proportional|tabular, figures?, justify?: left|center|right|justifyLeft|justifyCenter|justifyRight|justifyAll, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?: ltr|rtl, composer?: everyLine|singleLine, hangingPunctuation?, strokeOverFill?, box?: [x,y,w,h]|null (layer space), vertical?}",
|
||||
"{layer?, range?: [start, end] (characters; default all), text?, font?, style?, size?, fill?, stroke?, applyFill?, applyStroke?, strokeWidth?, tracking?, leading?: px|\"auto\", baselineShift? px, hScale? %, vScale? %, tsume? %, fauxBold?, fauxItalic?, allCaps?, smallCaps?, baseline?: normal|superscript|subscript, superscript?, subscript?, kerning?: metrics|optical|number, ligatures?, OpenType: discretionaryLigatures?, contextualAlternates?, stylisticAlternates?, stylisticSets?: [1–20], ss01…ss20?, swash?, titling?, ordinals?, fractions?, allSmallCaps?, figureStyle?: default|lining|oldStyle, figureWidth?: default|proportional|tabular, figures?, variations?: {tag: value} (variable font axes; null resets), justify?: left|center|right|justifyLeft|justifyCenter|justifyRight|justifyAll, indentLeft?, indentRight?, indentFirst?, spaceBefore?, spaceAfter?, direction?: ltr|rtl, composer?: everyLine|singleLine, hangingPunctuation?, strokeOverFill?, box?: [x,y,w,h]|null (layer space), vertical?}",
|
||||
has_layers,
|
||||
set_text
|
||||
),
|
||||
|
|
|
|||
|
|
@ -326,6 +326,51 @@ fn variable_font_axes_animator() {
|
|||
let _ = info;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_font_axes_in_the_character_style() {
|
||||
let mut s = comp(600, 200);
|
||||
let t = s.execute("layer.newText", json!({"text": "Hamburg", "size": 60})).unwrap()["layer"].as_u64().unwrap();
|
||||
// The attribute round-trips and is undoable even for static fonts (it is kept, unused).
|
||||
s.execute("layer.setText", json!({"layer": t, "variations": {"wght": 650}})).unwrap();
|
||||
let d = crate::commands::text_edit::layer_doc(&s, LayerId(t)).unwrap();
|
||||
assert_eq!(d.style_at(0).variations, vec![("wght".to_string(), 650.0)]);
|
||||
assert_eq!(effectcraft_keyframe::text_doc::char_style_json(&d.style_at(0))["variations"], json!({"wght": 650.0}));
|
||||
s.execute("layer.setText", json!({"layer": t, "range": [0, 3], "variations": {"wght": null}})).unwrap();
|
||||
let d = crate::commands::text_edit::layer_doc(&s, LayerId(t)).unwrap();
|
||||
assert!(d.style_at(0).variations.is_empty() && !d.style_at(5).variations.is_empty());
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(crate::commands::text_edit::layer_doc(&s, LayerId(t)).unwrap().style_at(0).variations.is_empty());
|
||||
assert!(s.execute("layer.setText", json!({"layer": t, "variations": 3})).is_err());
|
||||
let Some((face, axes)) = effectcraft_text::variable::find_variable_face() else {
|
||||
eprintln!("no variable font installed: rendering check skipped");
|
||||
return;
|
||||
};
|
||||
let info = effectcraft_text::fonts::face(face).info.clone();
|
||||
if effectcraft_text::resolve(&info.family, &info.style).face != face {
|
||||
eprintln!("variable face not reachable by family/style: skipped");
|
||||
return;
|
||||
}
|
||||
s.execute("layer.setText", json!({"layer": t, "font": info.family, "style": info.style})).unwrap();
|
||||
let a = axes.iter().find(|a| a.tag == "wght" && a.max > a.min).or_else(|| axes.iter().find(|a| a.max > a.min)).unwrap().clone();
|
||||
let geom = |s: &Session| {
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, comp, s.time());
|
||||
let l = comp.layer(LayerId(t)).unwrap();
|
||||
let paths = effectcraft_render::text::glyph_paths(&ctx, l);
|
||||
let area: f64 = paths.iter().map(|(p, _)| effectcraft_text::kurbo::Shape::area(p).abs()).sum();
|
||||
let right = paths.iter().map(|(p, _)| effectcraft_text::kurbo::Shape::bounding_box(p).x1).fold(f64::MIN, f64::max);
|
||||
(area, right)
|
||||
};
|
||||
s.execute("layer.setText", json!({"layer": t, "variations": {a.tag.trim_end(): a.min}})).unwrap();
|
||||
let lo = geom(&s);
|
||||
s.execute("layer.setText", json!({"layer": t, "variations": {a.tag.trim_end(): a.max}})).unwrap();
|
||||
let hi = geom(&s);
|
||||
assert!((hi.0 - lo.0).abs() > 1.0, "{} outlines {lo:?} → {hi:?}", a.tag);
|
||||
assert!((hi.1 - lo.1).abs() > 0.5, "{} advances {lo:?} → {hi:?}", a.tag);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_focus_commands_undo_and_serde() {
|
||||
let mut s = comp(400, 300);
|
||||
|
|
|
|||
|
|
@ -299,6 +299,10 @@ pub struct CharStyle {
|
|||
/// OpenType features (stylistic sets, figures, fractions…).
|
||||
#[serde(skip_serializing_if = "OpenType::is_default")]
|
||||
pub opentype: OpenType,
|
||||
/// Variable font axis values (tag, user units; axes not listed stay at their defaults):
|
||||
/// the Character panel's Variable Font Axes.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub variations: Vec<(String, f32)>,
|
||||
}
|
||||
|
||||
impl Default for CharStyle {
|
||||
|
|
@ -383,6 +387,9 @@ pub struct TextDoc {
|
|||
/// Base OpenType features (see [`CharStyle::opentype`]).
|
||||
#[serde(skip_serializing_if = "OpenType::is_default")]
|
||||
pub opentype: OpenType,
|
||||
/// Base variable font axis values (see [`CharStyle::variations`]).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub variations: Vec<(String, f32)>,
|
||||
pub indent_left: f64,
|
||||
pub indent_right: f64,
|
||||
pub indent_first: f64,
|
||||
|
|
@ -435,6 +442,7 @@ impl Default for TextDoc {
|
|||
tate_chu_yoko: false,
|
||||
vertical_roman_upright: false,
|
||||
opentype: OpenType::default(),
|
||||
variations: vec![],
|
||||
indent_left: 0.0,
|
||||
indent_right: 0.0,
|
||||
indent_first: 0.0,
|
||||
|
|
@ -522,6 +530,7 @@ impl TextDoc {
|
|||
tate_chu_yoko: self.tate_chu_yoko,
|
||||
vertical_roman_upright: self.vertical_roman_upright,
|
||||
opentype: self.opentype,
|
||||
variations: self.variations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -550,6 +559,7 @@ impl TextDoc {
|
|||
self.tate_chu_yoko = s.tate_chu_yoko;
|
||||
self.vertical_roman_upright = s.vertical_roman_upright;
|
||||
self.opentype = s.opentype;
|
||||
self.variations = s.variations.clone();
|
||||
}
|
||||
|
||||
/// The base (first paragraph's) settings.
|
||||
|
|
@ -951,6 +961,7 @@ pub const CHAR_ATTRS: &[&str] = &[
|
|||
"figureStyle",
|
||||
"figureWidth",
|
||||
"figures",
|
||||
"variations",
|
||||
];
|
||||
|
||||
/// Paragraph attribute keys of `layer.setText`.
|
||||
|
|
@ -1068,6 +1079,28 @@ pub fn apply_char_attr(s: &mut CharStyle, key: &str, v: &J) -> Result<bool, Stri
|
|||
s.opentype.figure_style = st;
|
||||
s.opentype.figure_width = w;
|
||||
}
|
||||
// Variable font axes: `{wght: 650, wdth: 90}` (null removes an axis), or `{}` / null to
|
||||
// reset every axis to its default.
|
||||
"variations" => match v {
|
||||
J::Null => s.variations.clear(),
|
||||
J::Object(m) => {
|
||||
if m.is_empty() {
|
||||
s.variations.clear();
|
||||
}
|
||||
for (tag, val) in m {
|
||||
if tag.is_empty() || tag.len() > 4 {
|
||||
return Err(format!("variations: `{tag}` is not an axis tag"));
|
||||
}
|
||||
s.variations.retain(|(t, _)| t != tag);
|
||||
match val {
|
||||
J::Null => {}
|
||||
_ => s.variations.push((tag.clone(), val.as_f64().ok_or("variations: expected {tag: number}")? as f32)),
|
||||
}
|
||||
}
|
||||
s.variations.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
}
|
||||
_ => return Err("variations: expected {tag: number, …}".into()),
|
||||
},
|
||||
k if k.len() == 4 && k.starts_with("ss") && k[2..].parse::<u32>().is_ok_and(|n| (1..=20).contains(&n)) => {
|
||||
let n = k[2..].parse::<u32>().unwrap_or(0);
|
||||
s.opentype.set_stylistic_set(n, flag(key, v)?);
|
||||
|
|
@ -1123,6 +1156,7 @@ pub fn char_style_json(s: &CharStyle) -> J {
|
|||
"stylisticSets": (1..=20u32).filter(|n| s.opentype.stylistic_set(*n)).collect::<Vec<_>>(),
|
||||
"swash": s.opentype.swash, "titling": s.opentype.titling, "ordinals": s.opentype.ordinals, "fractions": s.opentype.fractions,
|
||||
"allSmallCaps": s.opentype.all_small_caps,
|
||||
"variations": s.variations.iter().map(|(t, v)| (t.clone(), J::from(*v as f64))).collect::<serde_json::Map<String, J>>(),
|
||||
"figureStyle": match s.opentype.figure_style { FigureStyle::Default => "default", FigureStyle::Lining => "lining", FigureStyle::OldStyle => "oldStyle" },
|
||||
"figureWidth": match s.opentype.figure_width { FigureWidth::Default => "default", FigureWidth::Proportional => "proportional", FigureWidth::Tabular => "tabular" },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ pub struct TextStyle {
|
|||
pub leading: Option<f32>,
|
||||
/// OpenType features (stylistic sets, figures, fractions, all small caps…).
|
||||
pub opentype: OpenType,
|
||||
/// Variable font axis values (tag, user units): shaping (advances through HVAR / gvar
|
||||
/// phantom points) and outlines use this design-space position.
|
||||
pub variations: Vec<(String, f32)>,
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
|
|
@ -109,6 +112,7 @@ impl Default for TextStyle {
|
|||
script: Script::Normal,
|
||||
leading: None,
|
||||
opentype: OpenType::default(),
|
||||
variations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,6 +198,10 @@ impl Hash for TextStyle {
|
|||
self.leading.map(f32::to_bits).hash(h);
|
||||
(self.kerning, self.optical, self.ligatures, self.faux_bold, self.faux_italic, self.caps, self.underline, self.script).hash(h);
|
||||
self.opentype.hash(h);
|
||||
for (t, v) in &self.variations {
|
||||
t.hash(h);
|
||||
hf(h, *v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,6 +233,9 @@ pub struct Glyph {
|
|||
pub v_scale: f32,
|
||||
/// Index of the style run the glyph belongs to.
|
||||
pub run: usize,
|
||||
/// The run's variable font axis values, interned ([`crate::variable::coords`]; 0 = the
|
||||
/// default instance): outlines are drawn there.
|
||||
pub variations: u32,
|
||||
}
|
||||
|
||||
/// One laid-out line.
|
||||
|
|
@ -425,7 +436,13 @@ fn ink_x(face: FaceId, gid: u32) -> Option<(f32, f32)> {
|
|||
fn shape_item(chars: &[(usize, char, char)], rtl: bool, face: FaceId, size: f32, style: &TextStyle, run: usize, form: Form) -> Vec<ShapedGlyph> {
|
||||
let f = fonts::face(face);
|
||||
let (Some(font), Some(data)) = (f.font(), f.shaper_data()) else { return Vec::new() };
|
||||
let shaper = data.shaper(&font).build();
|
||||
let instance = (!style.variations.is_empty()).then(|| {
|
||||
harfrust::ShaperInstance::from_variations(
|
||||
&font,
|
||||
style.variations.iter().map(|(t, v)| harfrust::Variation { tag: crate::variable::tag_of(t), value: *v }),
|
||||
)
|
||||
});
|
||||
let shaper = data.shaper(&font).instance(instance.as_ref()).build();
|
||||
let mut buf = UnicodeBuffer::new();
|
||||
for &(b, _, sc) in chars {
|
||||
buf.add(if sc == '\t' { ' ' } else { sc }, b as u32);
|
||||
|
|
@ -819,6 +836,7 @@ fn paragraph(text: &str, base: usize, runs: &[(Range<usize>, TextStyle)], primar
|
|||
h_scale: st.h_scale,
|
||||
v_scale: st.v_scale,
|
||||
run: g.run,
|
||||
variations: crate::variable::intern(&st.variations),
|
||||
});
|
||||
pen += g.adv;
|
||||
if space_extra > 0.0 && text[g.cluster..].starts_with(' ') && g.cluster < ce {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ pub(crate) fn outline_units(face: FaceId, gid: u32) -> Option<Arc<BezPath>> {
|
|||
/// The outline of a laid-out glyph in pixels, origin on its baseline (y down), with its
|
||||
/// horizontal / vertical scale and faux italic.
|
||||
pub fn glyph_outline(g: &Glyph) -> BezPath {
|
||||
let Some(units) = outline_units(g.face, g.id) else { return BezPath::new() };
|
||||
let units = if g.variations == 0 { outline_units(g.face, g.id) } else { variable::outline_units_at(g.face, g.id, &variable::coords(g.variations)) };
|
||||
let Some(units) = units else { return BezPath::new() };
|
||||
glyph_affine(g) * (*units).clone()
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +108,8 @@ pub struct CharGlyph {
|
|||
pub face: FaceId,
|
||||
pub gid: u32,
|
||||
pub outline_xf: Affine,
|
||||
/// The character's variable font axis values (its style's Variable Font Axes).
|
||||
pub variations: Vec<(String, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
@ -162,6 +165,7 @@ pub fn text_style(s: &CharStyle) -> TextStyle {
|
|||
},
|
||||
leading: s.leading.map(|l| l as f32),
|
||||
opentype: s.opentype,
|
||||
variations: s.variations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -336,6 +340,7 @@ pub fn layout_doc(doc: &TextDoc) -> TextLayout {
|
|||
face: g.face,
|
||||
gid: g.id,
|
||||
outline_xf,
|
||||
variations: variable::coords(g.variations).to_vec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,32 @@ use skrifa::{GlyphId, MetadataProvider, Tag};
|
|||
use crate::fonts::{self, FaceId};
|
||||
use crate::{CharGlyph, Pen};
|
||||
|
||||
/// Intern a set of axis values (0 for none), so `Copy` glyphs can carry them.
|
||||
pub fn intern(coords: &[(String, f32)]) -> u32 {
|
||||
if coords.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut t = table().lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(i) = t.iter().position(|c| c.as_slice() == coords) {
|
||||
return i as u32 + 1;
|
||||
}
|
||||
t.push(Arc::new(coords.to_vec()));
|
||||
t.len() as u32
|
||||
}
|
||||
|
||||
/// The axis values of an interned id (empty for 0).
|
||||
pub fn coords(id: u32) -> Arc<Vec<(String, f32)>> {
|
||||
if id == 0 {
|
||||
return Arc::new(Vec::new());
|
||||
}
|
||||
table().lock().unwrap_or_else(|e| e.into_inner()).get(id as usize - 1).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn table() -> &'static Mutex<Vec<Arc<Vec<(String, f32)>>>> {
|
||||
static T: OnceLock<Mutex<Vec<Arc<Vec<(String, f32)>>>>> = OnceLock::new();
|
||||
T.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// One variation axis of a face, in user units.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FontAxis {
|
||||
|
|
@ -38,7 +64,7 @@ pub fn font_axes(face: FaceId) -> Vec<FontAxis> {
|
|||
}
|
||||
|
||||
/// A tag string as a [`Tag`] (padded with spaces, at most four characters).
|
||||
fn tag_of(s: &str) -> Tag {
|
||||
pub(crate) fn tag_of(s: &str) -> Tag {
|
||||
let mut b = [b' '; 4];
|
||||
for (i, c) in s.bytes().take(4).enumerate() {
|
||||
b[i] = c;
|
||||
|
|
@ -78,14 +104,21 @@ pub fn outline_units_at(face: FaceId, gid: u32, coords: &[(String, f32)]) -> Opt
|
|||
}
|
||||
|
||||
/// A laid-out character's outline (same placement as [`CharGlyph::path`]) with its face's axes
|
||||
/// moved by `deltas` (tag, user-unit offset from the axis default).
|
||||
/// moved by `deltas` (tag, user-unit offset from the character's own axis value — its style's
|
||||
/// Variable Font Axes setting, else the axis default).
|
||||
pub fn char_outline_varied(g: &CharGlyph, deltas: &[(String, f32)]) -> Option<BezPath> {
|
||||
let axes = font_axes(g.face);
|
||||
let coords: Vec<(String, f32)> =
|
||||
deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (a.default + d).clamp(a.min, a.max)))).collect();
|
||||
let base = |t: &str, d: f32| g.variations.iter().find(|(x, _)| x.trim_end() == t.trim_end()).map(|(_, v)| *v).unwrap_or(d);
|
||||
let mut coords: Vec<(String, f32)> =
|
||||
deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (base(&a.tag, a.default) + d).clamp(a.min, a.max)))).collect();
|
||||
if coords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for (t, v) in &g.variations {
|
||||
if !coords.iter().any(|(c, _)| c == t) {
|
||||
coords.push((t.clone(), *v));
|
||||
}
|
||||
}
|
||||
let units = outline_units_at(g.face, g.gid, &coords)?;
|
||||
Some(g.outline_xf * (*units).clone())
|
||||
}
|
||||
|
|
@ -122,4 +155,37 @@ mod tests {
|
|||
let s = crate::outline_units(face, gid).unwrap();
|
||||
assert!((d.area() - s.area()).abs() < 1e-3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_variations_change_advances_and_outlines() {
|
||||
assert_eq!(intern(&[]), 0);
|
||||
let id = intern(&[("wght".into(), 700.0)]);
|
||||
assert_eq!(intern(&[("wght".into(), 700.0)]), id);
|
||||
assert_eq!(coords(id).as_slice(), &[("wght".to_string(), 700.0)]);
|
||||
let Some((face, axes)) = find_variable_face() else {
|
||||
eprintln!("no variable font installed: skipped");
|
||||
return;
|
||||
};
|
||||
// An axis that changes widths (wdth or wght, else the first that moves).
|
||||
let a = axes.iter().find(|a| a.tag == "wdth" && a.max > a.min).or_else(|| axes.iter().find(|a| a.tag == "wght" && a.max > a.min)).unwrap_or(&axes[0]);
|
||||
let info = crate::fonts::face(face).info.clone();
|
||||
let st = |v: f32| crate::TextStyle {
|
||||
family: info.family.clone(),
|
||||
style: info.style.clone(),
|
||||
size: 100.0,
|
||||
variations: vec![(a.tag.clone(), v)],
|
||||
..Default::default()
|
||||
};
|
||||
let (lo, hi) = (crate::layout::measure("Hamburgefonts", &st(a.min)), crate::layout::measure("Hamburgefonts", &st(a.max)));
|
||||
if crate::resolve(&info.family, &info.style).face != face {
|
||||
eprintln!("variable face not reachable by family/style: skipped");
|
||||
return;
|
||||
}
|
||||
assert!((lo - hi).abs() > 1.0, "{} {}..{}: advances {lo} vs {hi}", a.tag, a.min, a.max);
|
||||
let ink = |v: f32| {
|
||||
let l = crate::layout::layout("H", &st(v), &crate::ParagraphStyle::default());
|
||||
l.glyphs.iter().map(|g| crate::glyph_outline(g).area().abs()).sum::<f64>()
|
||||
};
|
||||
assert!((ink(a.min) - ink(a.max)).abs() > 1.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,29 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
p.text(pos2(cr.max.x + 6.0, y + 7.0), Align2::LEFT_CENTER, label, Tokens::ui(11.0), t.text_dim);
|
||||
}
|
||||
}
|
||||
// Variable Font Axes: one value per axis of a variable font (`character.axis.<tag>`).
|
||||
let face = effectcraft_engine::text::resolve(&doc.font, &doc.style).face;
|
||||
let axes = effectcraft_engine::text::variable::font_axes(face);
|
||||
if !axes.is_empty() {
|
||||
y += 26.0;
|
||||
p.text(pos2(x0, y + 7.0), Align2::LEFT_CENTER, "Variable Font Axes", Tokens::semibold(11.0), t.text_dim);
|
||||
for (i, a) in axes.iter().enumerate() {
|
||||
let (col, row) = (i % 2, i / 2);
|
||||
let fx = x0 + col as f32 * (w / 2.0);
|
||||
let fy = y + 20.0 + row as f32 * 24.0;
|
||||
let tag = a.tag.trim_end().to_string();
|
||||
let v = doc.variations.iter().find(|(t2, _)| t2.trim_end() == tag).map(|(_, v)| *v).unwrap_or(a.default);
|
||||
p.text(pos2(fx, fy + 9.0), Align2::LEFT_CENTER, &tag, Tokens::semibold(10.5), t.text_dim);
|
||||
let (r, nv, _) =
|
||||
widgets::hot_number_at(ui, pos2(fx + 34.0, fy), egui::Id::new(("char-axis", &tag)), v as f64, 1.0, (a.min as f64, a.max as f64), 0, "", &t);
|
||||
app.auto.add(&format!("character.axis.{tag}"), r, &a.name);
|
||||
if let Some(nv) = nv
|
||||
&& enabled
|
||||
{
|
||||
actions.push(json!({"variations": {tag.clone(): nv}, "merge": format!("char-axis-{tag}")}));
|
||||
}
|
||||
}
|
||||
}
|
||||
match &target {
|
||||
None => {
|
||||
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, "Select a text layer", Tokens::ui(11.0), t.text_faint);
|
||||
|
|
|
|||
Loading…
Reference in New Issue