mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
Merge #1283
This commit is contained in:
@@ -42,6 +42,29 @@ pub fn field_type(kind: &StyleKind, key: &str) -> Option<VariableType> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The variable scopes a picker offers for `key` of a style of `kind`: a variable scoped to any one
|
||||
/// of them fits. A paint style's colors take fill and stroke variables, an effect style's fields
|
||||
/// take effect variables, and a text style's fields take the variables of their font property.
|
||||
/// None when every variable of the field's type fits (a grid's fields, as on layers) or the style
|
||||
/// has no such field.
|
||||
pub fn field_scopes(kind: &StyleKind, key: &str) -> Option<&'static [&'static str]> {
|
||||
field_type(kind, key)?;
|
||||
Some(match (kind, list_binding(key)) {
|
||||
(StyleKind::Paint { .. }, _) => &["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"],
|
||||
(StyleKind::Effect { .. }, Some((_, _, "color"))) => &["EFFECT_COLOR"],
|
||||
(StyleKind::Effect { .. }, _) => &["EFFECT_FLOAT"],
|
||||
(StyleKind::Text { .. }, _) => match key {
|
||||
"fontSize" => &["FONT_SIZE"],
|
||||
"lineHeight" => &["LINE_HEIGHT"],
|
||||
"letterSpacing" => &["LETTER_SPACING"],
|
||||
"fontFamily" => &["FONT_FAMILY"],
|
||||
"fontStyle" => &["FONT_STYLE"],
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The key a style stores a binding of `key` under: the first paint's colour has one spelling.
|
||||
pub fn canonical(key: &str) -> String {
|
||||
match list_binding(key) {
|
||||
|
||||
@@ -182,6 +182,46 @@ fn what_a_style_cannot_bind_is_refused() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_fields_offer_their_properties_scopes() {
|
||||
let mut s = Session::new();
|
||||
let (_, _, a, _, sid) = two_modes(&mut s);
|
||||
run(&mut s, "effect.add", json!({"ids": [a], "type": "dropShadow"}));
|
||||
let fx = run(&mut s, "style.create", json!({"type": "effect", "name": "Drop", "from": a}))["id"].as_str().unwrap().to_string();
|
||||
let light = node(&s, a).parent.unwrap();
|
||||
run(&mut s, "grid.add", json!({"ids": [light]}));
|
||||
let grid = run(&mut s, "style.create", json!({"type": "grid", "name": "Base", "from": light}))["id"].as_str().unwrap().to_string();
|
||||
let body = run(&mut s, "style.create", json!({"type": "text", "name": "Body", "value": {"fontFamily": "Inter", "fontSize": 14}}))["id"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let doc = s.doc().unwrap();
|
||||
let scopes = |st: &str, key: &str| crate::style_vars::field_scopes(&doc.style(st).unwrap().kind, key);
|
||||
let fills: &[&str] = &["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
|
||||
for (st, key, want) in [
|
||||
(&sid, "fill", Some(fills)),
|
||||
(&sid, "fills/0/color", Some(fills)),
|
||||
(&fx, "effects/0/color", Some(&["EFFECT_COLOR"][..])),
|
||||
(&fx, "effects/0/radius", Some(&["EFFECT_FLOAT"][..])),
|
||||
(&fx, "effects/0/offsetX", Some(&["EFFECT_FLOAT"][..])),
|
||||
(&body, "fontSize", Some(&["FONT_SIZE"][..])),
|
||||
(&body, "lineHeight", Some(&["LINE_HEIGHT"][..])),
|
||||
(&body, "letterSpacing", Some(&["LETTER_SPACING"][..])),
|
||||
(&body, "fontFamily", Some(&["FONT_FAMILY"][..])),
|
||||
(&body, "fontStyle", Some(&["FONT_STYLE"][..])),
|
||||
// Grid fields take any number variable, as on layers; fields a style lacks take none.
|
||||
(&grid, "layoutGrids/0/size", None),
|
||||
(&sid, "fills/3/color", None),
|
||||
(&body, "opacity", None),
|
||||
] {
|
||||
assert_eq!(scopes(st, key), want, "{st} {key}");
|
||||
}
|
||||
// As on layers, scopes limit pickers only: an out-of-scope variable still binds.
|
||||
run(&mut s, "variable.update", json!({"variable": "size", "scopes": ["CORNER_RADIUS"]}));
|
||||
run(&mut s, "style.bind", json!({"style": body, "field": "fontSize", "variable": "size"}));
|
||||
assert!(s.doc().unwrap().style(&body).unwrap().bound.contains_key("fontSize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_and_fig_carry_style_bindings() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -555,7 +555,11 @@ fn listing(app: &FigApp, ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut
|
||||
}
|
||||
}
|
||||
let colors = if slot.paints() {
|
||||
crate::variable_ui::groups(doc, &libs, |_, v| v.kind == VariableType::Color && (q.is_empty() || v.name.to_lowercase().contains(&q)))
|
||||
// Variables scoped to the selection's fill (or stroke), as the Design panel's pickers offer.
|
||||
let scope = p.ids.first().and_then(|id| figcraft_engine::cmd::variables::field_scope(doc, *id, slot.key()));
|
||||
crate::variable_ui::groups(doc, &libs, |_, v| {
|
||||
v.kind == VariableType::Color && (q.is_empty() || v.name.to_lowercase().contains(&q)) && scope.is_none_or(|s| v.in_scope(s))
|
||||
})
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
@@ -399,6 +399,11 @@ pub fn picker(app: &mut FigApp, ctx: &egui::Context) {
|
||||
Target::Alias { variable, .. } => (None, doc.variables.get(variable)),
|
||||
Target::Proto(_) | Target::Style { .. } => (None, None),
|
||||
};
|
||||
// A style's fields offer variables scoped to one of the properties the field sets.
|
||||
let style_scopes = match &p.target {
|
||||
Target::Style { style, field } => doc.style(style).and_then(|st| figcraft_engine::style_vars::field_scopes(&st.kind, field)),
|
||||
_ => None,
|
||||
};
|
||||
// Prototypes and styles use this file's variables.
|
||||
let local_only = matches!(p.target, Target::Proto(_) | Target::Style { .. });
|
||||
let field = match &p.target {
|
||||
@@ -410,6 +415,7 @@ pub fn picker(app: &mut FigApp, ctx: &egui::Context) {
|
||||
&& !(local_only && lib.is_some())
|
||||
&& (q.is_empty() || v.name.to_lowercase().contains(&q))
|
||||
&& scope.is_none_or(|s| v.in_scope(s))
|
||||
&& style_scopes.is_none_or(|list| list.iter().any(|s| v.in_scope(s)))
|
||||
&& (lib.is_some() || own.is_none_or(|o| figcraft_engine::cmd::variables::alias_target(&doc.variables, o, &v.id).is_ok()))
|
||||
};
|
||||
let groups = groups(&doc, &libs, keep);
|
||||
@@ -518,3 +524,113 @@ mod kind_tests {
|
||||
assert!(kind_fits(None, None, Boolean));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod scope_tests {
|
||||
use figcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
use crate::layers_drag_tests::Ui as TestUi;
|
||||
use crate::style_picker::StyleSlot;
|
||||
|
||||
fn run(s: &mut Session, cmd: &str, p: serde_json::Value) -> serde_json::Value {
|
||||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
||||
}
|
||||
|
||||
/// A rectangle (selected, with a drop shadow), a frame with a layout grid, the styles "Brand"
|
||||
/// (paint), "Shadow" (effect, from the rectangle), "Base" (grid, from the frame) and "Body"
|
||||
/// (text), and variables each scoped to one property (`any` to none).
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
let r = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 40}))["id"].clone();
|
||||
run(&mut s, "effect.add", json!({"ids": [r], "type": "dropShadow"}));
|
||||
let f = run(&mut s, "frame.create", json!({"x": 100, "y": 0, "width": 100, "height": 100}))["id"].clone();
|
||||
run(&mut s, "grid.add", json!({"ids": [f]}));
|
||||
run(&mut s, "style.create", json!({"type": "paint", "name": "Brand", "value": [{"type": "solid", "color": "#0D99FF"}]}));
|
||||
run(&mut s, "style.create", json!({"type": "effect", "name": "Shadow", "from": r}));
|
||||
run(&mut s, "style.create", json!({"type": "grid", "name": "Base", "from": f}));
|
||||
run(&mut s, "style.create", json!({"type": "text", "name": "Body", "value": {"fontFamily": "Inter", "fontSize": 14}}));
|
||||
run(&mut s, "variable.createCollection", json!({"name": "Tokens", "modes": ["Base"]}));
|
||||
for (name, ty, value, scope) in [
|
||||
("frame fill", "color", json!("#FF0000"), Some("FRAME_FILL")),
|
||||
("shape fill", "color", json!("#00FF00"), Some("SHAPE_FILL")),
|
||||
("stroke color", "color", json!("#0000FF"), Some("STROKE_COLOR")),
|
||||
("shadow color", "color", json!("#000000"), Some("EFFECT_COLOR")),
|
||||
("radius", "number", json!(8), Some("CORNER_RADIUS")),
|
||||
("font size", "number", json!(18), Some("FONT_SIZE")),
|
||||
("blur", "number", json!(4), Some("EFFECT_FLOAT")),
|
||||
("any", "number", json!(2), None),
|
||||
("family", "string", json!("Inter"), Some("FONT_FAMILY")),
|
||||
("weight", "string", json!("Bold"), Some("FONT_STYLE")),
|
||||
] {
|
||||
run(&mut s, "variable.create", json!({"collection": "Tokens", "name": name, "type": ty, "values": {"Base": value}}));
|
||||
if let Some(sc) = scope {
|
||||
run(&mut s, "variable.update", json!({"variable": name, "scopes": [sc]}));
|
||||
}
|
||||
}
|
||||
run(&mut s, "select.set", json!({"ids": [r]}));
|
||||
s
|
||||
}
|
||||
|
||||
/// The texts drawn in one frame.
|
||||
fn drawn(u: &mut TestUi) -> Vec<String> {
|
||||
fn walk(shape: &egui::Shape, out: &mut Vec<String>) {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for c in u.frame_output(vec![]).shapes {
|
||||
walk(&c.shape, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn check(texts: &[String], shown: &[&str], hidden: &[&str], what: &str) {
|
||||
for name in shown {
|
||||
assert!(texts.iter().any(|t| t == name), "{what}: {name:?} offered, in {texts:?}");
|
||||
}
|
||||
for name in hidden {
|
||||
assert!(!texts.iter().any(|t| t == name), "{what}: {name:?} not offered, in {texts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_styles_variable_picker_offers_the_fields_scopes_only() {
|
||||
use VariableType::{Color, Number, String};
|
||||
let mut u = TestUi::new(session());
|
||||
// (style, field, type, offered, not offered)
|
||||
for (style, field, kind, shown, hidden) in [
|
||||
("Brand", "fill", Color, &["frame fill", "shape fill", "stroke color"][..], &["shadow color"][..]),
|
||||
("Shadow", "effects/0/color", Color, &["shadow color"], &["frame fill", "shape fill", "stroke color"]),
|
||||
("Shadow", "effects/0/radius", Number, &["blur", "any"], &["radius", "font size"]),
|
||||
("Base", "layoutGrids/0/size", Number, &["radius", "font size", "blur", "any"], &[]),
|
||||
("Body", "fontSize", Number, &["font size", "any"], &["radius", "blur"]),
|
||||
("Body", "fontFamily", String, &["family"], &["weight"]),
|
||||
("Body", "fontStyle", String, &["weight"], &["family"]),
|
||||
] {
|
||||
let id = u.app.session.doc().unwrap().style(style).unwrap().id.clone();
|
||||
let target = Target::Style { style: id, field: field.to_string() };
|
||||
u.ctx.data_mut(|d| d.insert_temp(picker_id(), Picker { target, kind: Some(kind), at: pos2(400.0, 100.0) }));
|
||||
u.frame(vec![]);
|
||||
check(&drawn(&mut u), shown, hidden, &format!("{style} {field}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fill_and_stroke_style_pickers_offer_scoped_color_variables() {
|
||||
let mut u = TestUi::new(session());
|
||||
for (slot, shown, hidden) in [
|
||||
(StyleSlot::Fill, &["shape fill"][..], &["frame fill", "stroke color", "shadow color"][..]),
|
||||
(StyleSlot::Stroke, &["stroke color"], &["frame fill", "shape fill", "shadow color"]),
|
||||
] {
|
||||
crate::style_picker::open_for_selection(&mut u.app, slot, None);
|
||||
u.frame(vec![]);
|
||||
let texts = drawn(&mut u);
|
||||
assert!(texts.iter().any(|t| t == "Color variables"), "{slot:?}: {texts:?}");
|
||||
check(&texts, shown, hidden, &format!("{slot:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user