mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
Number fields for opacity (variables hold percentages), individual corner radii, auto layout padding and gaps, min/max sizes and font size now take variables like width and corner radius already did. Text content binds to string variables and visibility to boolean ones from the Typography and Appearance section headers; bound ones show as a variable chip that changes or detaches the binding. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.9 KiB
Rust
69 lines
3.9 KiB
Rust
//! Variable bindings through the session: binding, value edits and modes reach the layers.
|
|
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::params::id_of;
|
|
|
|
#[test]
|
|
fn bound_layers_follow_variable_values_and_unbind() {
|
|
let mut s = Session::new();
|
|
let r = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"]).unwrap();
|
|
s.execute("variable.createCollection", &json!({"name": "Tokens", "modes": ["Light", "Dark"]})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Tokens", "name": "size/lg", "type": "number", "value": 48})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Tokens", "name": "brand", "type": "color", "value": "#0D99FF"})).unwrap();
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "width", "variable": "size/lg"})).unwrap();
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "fill", "variable": "brand"})).unwrap();
|
|
let node = |s: &Session| s.doc().unwrap().node(r).unwrap().clone();
|
|
assert_eq!(node(&s).width, 48.0);
|
|
assert_eq!(node(&s).fills.first().and_then(|p| p.solid_color()).map(|c| c.to_hex()), Some("0D99FF".into()));
|
|
// Editing the variable updates the bound layer.
|
|
s.execute("variable.set", &json!({"variable": "size/lg", "value": 64})).unwrap();
|
|
assert_eq!(node(&s).width, 64.0);
|
|
// A mode with another value applies through the page.
|
|
s.execute("variable.set", &json!({"variable": "size/lg", "mode": "Dark", "value": 80})).unwrap();
|
|
s.execute("variable.setMode", &json!({"collection": "Tokens", "mode": "Dark"})).unwrap();
|
|
assert_eq!(node(&s).width, 80.0);
|
|
// Unbinding keeps the value and stops following.
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "width", "variable": null})).unwrap();
|
|
s.execute("variable.set", &json!({"variable": "size/lg", "mode": "Dark", "value": 99})).unwrap();
|
|
assert_eq!(node(&s).width, 80.0);
|
|
assert!(s.execute("variable.bind", &json!({"ids": [r], "field": "width", "variable": "nope"})).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn opacity_padding_text_and_visibility_follow_their_variables() {
|
|
let mut s = Session::new();
|
|
let f = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"]).unwrap();
|
|
s.execute("layout.add", &json!({"ids": [f]})).unwrap();
|
|
let t = id_of(&s.execute("text.create", &json!({"text": "Hello", "x": 200, "y": 0})).unwrap()["id"]).unwrap();
|
|
s.execute("variable.createCollection", &json!({"name": "Tokens"})).unwrap();
|
|
for (name, kind, value) in
|
|
[("fade", "number", json!(40)), ("pad", "number", json!(12)), ("label", "string", json!("Sign in")), ("shown", "boolean", json!(false))]
|
|
{
|
|
s.execute("variable.create", &json!({"collection": "Tokens", "name": name, "type": kind, "value": value})).unwrap();
|
|
}
|
|
let bind = |s: &mut Session, id, field: &str, var: &str| {
|
|
s.execute("variable.bind", &json!({"ids": [id], "field": field, "variable": var})).unwrap();
|
|
};
|
|
bind(&mut s, f, "opacity", "fade");
|
|
bind(&mut s, f, "paddingHorizontal", "pad");
|
|
bind(&mut s, t, "characters", "label");
|
|
bind(&mut s, t, "visible", "shown");
|
|
let doc = s.doc().unwrap();
|
|
let frame = doc.node(f).unwrap();
|
|
// Opacity variables are percentages.
|
|
assert!((frame.opacity - 0.4).abs() < 1e-6, "{}", frame.opacity);
|
|
let pad = frame.layout.as_ref().unwrap().padding;
|
|
assert_eq!((pad[1], pad[3]), (12.0, 12.0));
|
|
let text = doc.node(t).unwrap();
|
|
assert_eq!(text.kind.text().map(|x| x.characters.as_str()), Some("Sign in"));
|
|
assert!(!text.visible);
|
|
// Out-of-range percentages clamp; booleans flip visibility back.
|
|
s.execute("variable.set", &json!({"variable": "fade", "value": 250})).unwrap();
|
|
s.execute("variable.set", &json!({"variable": "shown", "value": true})).unwrap();
|
|
let doc = s.doc().unwrap();
|
|
assert_eq!(doc.node(f).unwrap().opacity, 1.0);
|
|
assert!(doc.node(t).unwrap().visible);
|
|
}
|