Merge upstream/main into feat/text-direction-1830

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Trevor BrownandClaude Fable 5.1 committed 2026-11-05 16:41:34 -05:00
commit 7c6bc49e9f
35 files changed
+1259 -72

No files matched your search

+1
View File
@@ -33,6 +33,7 @@ pub fn field_type(kind: &StyleKind, key: &str) -> Option<VariableType> {
(EffectKind::DropShadow(_) | EffectKind::InnerShadow(_), "color") => Some(Color),
(EffectKind::DropShadow(_) | EffectKind::InnerShadow(_), "radius" | "spread" | "offsetX" | "offsetY") => Some(Number),
(EffectKind::LayerBlur(_) | EffectKind::BackgroundBlur(_), "radius" | "startRadius") => Some(Number),
(EffectKind::Glass(_), f) if figcraft_doc::Glass::BOUND_FIELDS.contains(&f) => Some(Number),
_ => None,
},
(StyleKind::Grid { grids }, Some((BoundList::Grids, i, "size" | "count" | "gutter" | "offset"))) => grids.get(i).map(|_| Number),
+35
View File
@@ -98,6 +98,41 @@ fn effect_moves_and_removals_reject_junk() {
assert_eq!(s.doc().unwrap().node(r).unwrap().effects.len(), 1);
}
fn glass(s: &Session, r: NodeId) -> figcraft_doc::Glass {
match s.doc().unwrap().node(r).unwrap().effects.first().map(|e| &e.kind) {
Some(EffectKind::Glass(g)) => g.clone(),
k => panic!("{k:?}"),
}
}
#[test]
fn glass_fields_take_number_variables() {
let (mut s, r) = setup();
ex(&mut s, "effect.add", json!({"ids": [r], "type": "glass"}));
// Percent fields read the variable as the Design panel shows them; `radius` is the frost.
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/splay", "variable": "large"}));
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/radius", "variable": "small"}));
ex(&mut s, "variable.bind", json!({"ids": [r], "field": "effects/0/depth", "variable": "large"}));
let g = glass(&s, r);
assert_eq!((g.splay, g.frost, g.depth), (0.3, 2.0, 30.0));
// A new value follows, clamped to the field's range (frost stops at 100).
ex(&mut s, "variable.set", json!({"variable": "small", "value": 150}));
ex(&mut s, "variable.set", json!({"variable": "large", "value": 75}));
let g = glass(&s, r);
assert_eq!((g.splay, g.frost, g.depth), (0.75, figcraft_doc::Glass::MAX_FROST, 75.0));
// A colour variable doesn't fit a glass field.
assert!(s.execute("variable.bind", &json!({"ids": [r], "field": "effects/0/refraction", "variable": "brand"})).is_err());
// An effect style binds them too, and every layer using it follows.
let fx = ex_value(&mut s, "style.create", json!({"type": "effect", "name": "Pane", "from": r}))["id"].as_str().unwrap().to_string();
ex(&mut s, "style.bind", json!({"style": fx, "field": "effects/0/refraction", "variable": "large"}));
assert_eq!(glass(&s, r).refraction, 0.75);
assert!(s.execute("style.bind", &json!({"style": fx, "field": "effects/0/spread", "variable": "large"})).is_err(), "glass has no spread");
}
fn ex_value(s: &mut Session, cmd: &str, p: serde_json::Value) -> serde_json::Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
fn fill_hex(s: &Session, r: NodeId) -> Vec<String> {
s.doc().unwrap().node(r).unwrap().fills.iter().filter_map(|p| p.solid_color()).map(|c| c.to_hex()).collect()
}
+2 -1
View File
@@ -465,7 +465,8 @@ pub fn scale_node(doc: &mut Document, id: NodeId, k: f64) -> Result<()> {
}
EffectKind::Glass(g) => {
g.depth *= k;
g.frost *= k;
// From the frost it draws with (an older file's past 100 draws at 100).
g.frost = (g.sanitized().frost * k).min(figcraft_doc::Glass::MAX_FROST);
}
}
}
+5 -2
View File
@@ -53,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"style.bind",
"Bind variable in style",
"{style, field, variable? (id or name; none: unbind)} bind a variable to a field of the style's value: a paint style's `fills/<i>/color` (or `fill`: its first paint) and `fills/<i>/stops/<j>/color`, an effect style's `effects/<i>/<color|radius|spread|offsetX|offsetY|startRadius>`, a grid style's `layoutGrids/<i>/<size|count|gutter|offset>`, a text style's `fontSize|lineHeight|letterSpacing` (numbers, px) and `fontFamily|fontStyle` (strings). Every layer using the style shows the field resolved in its own modes; the style keeps it resolved in the default modes",
"{style, field, variable? (id or name; none: unbind)} bind a variable to a field of the style's value: a paint style's `fills/<i>/color` (or `fill`: its first paint) and `fills/<i>/stops/<j>/color`, an effect style's `effects/<i>/<color|radius|spread|offsetX|offsetY|startRadius>` (glass: `lightAngle|lightIntensity|refraction|depth|dispersion|radius|splay`), a grid style's `layoutGrids/<i>/<size|count|gutter|offset>`, a text style's `fontSize|lineHeight|letterSpacing` (numbers, px) and `fontFamily|fontStyle` (strings). Every layer using the style shows the field resolved in its own modes; the style keeps it resolved in the default modes",
true,
bind_style
),
@@ -196,7 +196,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"variable.bind",
"Bind variable",
"{ids?, field (fill|stroke (first paint)|fills/<i>/color|strokes/<i>/color|fills/<i>/stops/<j>/color (gradient stops)|effects/<i>/<offsetX|offsetY|radius|startRadius|spread|color>|layoutGrids/<i>/<size|count|gutter|offset>|componentProperties/<name> (instances: boolean/text properties; variant properties take string variables naming a value)|width|height|opacity (percent)|cornerRadius|gap|paddingHorizontal|characters|visible|fontSize…; list indices bottom first), variable (null unbinds)}",
"{ids?, field (fill|stroke (first paint)|fills/<i>/color|strokes/<i>/color|fills/<i>/stops/<j>/color (gradient stops)|effects/<i>/<offsetX|offsetY|radius|startRadius|spread|color|lightAngle|lightIntensity|refraction|depth|dispersion|splay> (glass: `radius` is its frost; intensity, refraction, dispersion and splay in percent)|layoutGrids/<i>/<size|count|gutter|offset>|componentProperties/<name> (instances: boolean/text properties; variant properties take string variables naming a value)|width|height|opacity (percent)|cornerRadius|gap|paddingHorizontal|characters|visible|fontSize…; list indices bottom first), variable (null unbinds)}",
true,
bind
),
@@ -1304,6 +1304,9 @@ pub(crate) fn set_effect_field(effects: &mut [Effect], i: usize, prop: &str, val
(EffectKind::DropShadow(s) | EffectKind::InnerShadow(s), "color", _, VariableValue::Color(c)) => s.color = *c,
(EffectKind::LayerBlur(b) | EffectKind::BackgroundBlur(b), "radius", Some(x), _) => b.radius = x.max(0.0),
(EffectKind::LayerBlur(b) | EffectKind::BackgroundBlur(b), "startRadius", Some(x), _) => b.start_radius = x.max(0.0),
(EffectKind::Glass(g), field, Some(x), _) => {
g.set_bound(field, x);
}
_ => {}
}
}
+1 -1
View File
@@ -475,7 +475,7 @@ pub fn properties_with(doc: &Document, id: NodeId, fmt: ColorFormat) -> Vec<Sect
pct(g.refraction),
num(g.depth),
pct(g.dispersion),
num(g.frost),
num(g.sanitized().frost),
pct(g.splay)
),
),
+26
View File
@@ -95,6 +95,32 @@ fn scaling_scales_grain_and_glass() {
assert_eq!((g.refraction, nz.density), (d.refraction, 0.5));
}
#[test]
fn scaling_a_frost_from_an_older_file_starts_from_what_it_draws() {
// A frost of 150, saved before the 0–100 range, draws at 100: halved, it draws at 50.
let mut s = Session::new();
let r = rect(&mut s);
s.execute("effect.add", &json!({"ids": [r], "type": "glass"})).unwrap();
s.execute("effect.set", &json!({"ids": [r], "index": 0, "effect": {"type": "glass", "frost": 150}})).unwrap();
s.execute("transform.scale", &json!({"ids": [r], "factor": 0.5})).unwrap();
let EffectKind::Glass(g) = &s.doc().unwrap().node(r).unwrap().effects[0].kind else { panic!() };
assert_eq!(g.frost, 50.0);
}
#[test]
fn inspect_shows_a_frost_from_an_older_file_as_it_draws() {
// A frost of 150, saved before the 0–100 range, draws at 100: Dev Mode says 100 as CSS does.
let mut s = Session::new();
let r = rect(&mut s);
s.execute("effect.add", &json!({"ids": [r], "type": "glass"})).unwrap();
s.execute("effect.set", &json!({"ids": [r], "index": 0, "effect": {"type": "glass", "frost": 150}})).unwrap();
let doc = s.doc().unwrap();
let sections = crate::devinspect::properties(doc, r);
let glass = &sections.iter().find(|s| s.title == "Effects").unwrap().rows[0];
assert!(glass.value.contains(" frost 100 "), "{}", glass.value);
assert!(code_for(doc, r, CodeLang::Css).contains("backdrop-filter: blur(50px);"));
}
#[test]
fn inspect_and_css_show_the_new_effects() {
let mut s = Session::new();