mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
Selection colors: Create style keeps the variables bound to the row's paint
selection.createColorStyle gives the new style the variable bindings of the fill or stroke it is made from, the color and the gradient stops, so linked layers keep following the variables. Targets bound to different variables are refused, and the Styles menu leaves Create style out for them.
This commit is contained in:
1 parent
1b7d9221e2
commit
cb429813f3
3 files changed
+83
-15
No files matched your search
@@ -5,7 +5,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use figcraft_color::{Paint, PaintKind};
|
||||
use figcraft_doc::{Document, NodeId, NodeKind, Style, StyleKind, VariableType, VariableValue};
|
||||
use figcraft_doc::{Document, NodeId, NodeKind, Style, StyleKind, VariableId, VariableType, VariableValue};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -134,7 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"selection.createColorStyle",
|
||||
"Create selection color style",
|
||||
"{targets, name, description?} → {id}; makes a color style of the paint the targets share and applies it to them as selection.applyColorStyle does; a name another style has, or a target whose list holds other paints, is refused",
|
||||
"{targets, name, description?} → {id}; makes a color style of the paint the targets share and applies it to them as selection.applyColorStyle does, keeping the variables bound to the paint; a name another style has, a target whose list holds other paints, or targets bound to different variables are refused",
|
||||
true,
|
||||
create_style
|
||||
),
|
||||
@@ -259,15 +259,11 @@ fn create_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if doc.styles.iter().any(|st| st.name == name) {
|
||||
return Err(bad(CMD, format!("a style named `{name}` already exists")));
|
||||
}
|
||||
let kind = StyleKind::Paint { paints: vec![paint] };
|
||||
let bound = targets.first().map(|t| target_bindings(doc, t, &kind)).unwrap_or_default();
|
||||
let doc = s.doc_mut()?;
|
||||
let id = doc.new_style_id();
|
||||
let style = Style {
|
||||
id: id.clone(),
|
||||
name,
|
||||
description: string(p, "description").unwrap_or_default(),
|
||||
kind: StyleKind::Paint { paints: vec![paint] },
|
||||
bound: BTreeMap::new(),
|
||||
};
|
||||
let style = Style { id: id.clone(), name, description: string(p, "description").unwrap_or_default(), kind, bound };
|
||||
doc.styles.push(style.clone());
|
||||
link_style(s, &targets, &style)?;
|
||||
Ok(json!({"id": id}))
|
||||
@@ -275,12 +271,27 @@ fn create_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Why a color style can't be made from `targets` (`selection.createColorStyle`), if it can't: a
|
||||
/// linked style replaces the whole fill or stroke list, so a target whose list holds other paints
|
||||
/// would lose them. The Selection colors menu leaves Create style out for such rows.
|
||||
/// would lose them. The new style takes over the variables bound to the paint, so targets bound to
|
||||
/// different variables are refused too. The Selection colors menu leaves Create style out for such rows.
|
||||
pub fn create_refusal(doc: &Document, targets: &[PaintTarget]) -> Option<&'static str> {
|
||||
targets
|
||||
.iter()
|
||||
.any(|t| t.paints(doc).is_none_or(|paints| paints.len() != 1))
|
||||
.then_some("a style can only be made from a fill or stroke list that holds this one paint")
|
||||
if targets.iter().any(|t| t.paints(doc).is_none_or(|paints| paints.len() != 1)) {
|
||||
return Some("a style can only be made from a fill or stroke list that holds this one paint");
|
||||
}
|
||||
let paint = targets.first().and_then(|t| t.paint(doc))?;
|
||||
let kind = StyleKind::Paint { paints: vec![paint] };
|
||||
let mut bindings = targets.iter().map(|t| target_bindings(doc, t, &kind));
|
||||
let first = bindings.next()?;
|
||||
bindings.any(|b| b != first).then_some("the targets are bound to different variables")
|
||||
}
|
||||
|
||||
/// The variable bindings a paint style of `kind` made from `target` takes over: those on the fill or
|
||||
/// stroke list the target stands for. A text range with fills of its own holds none.
|
||||
fn target_bindings(doc: &Document, target: &PaintTarget, kind: &StyleKind) -> BTreeMap<String, VariableId> {
|
||||
if list_field(doc, target).is_none() {
|
||||
return BTreeMap::new();
|
||||
}
|
||||
let field = if target.fills { "fillStyle" } else { "strokeStyle" };
|
||||
doc.node(target.id).map(|n| crate::style_vars::from_layer(n, field, kind)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Apply paint style `style` to every target: a fill or stroke list is linked to it; a text range
|
||||
|
||||
@@ -529,6 +529,63 @@ fn creating_a_style_refuses_a_row_whose_list_holds_other_paints() {
|
||||
assert!(s.doc().unwrap().node(b).unwrap().styles.fill.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_style_created_from_a_variable_bound_row_keeps_following_the_variable() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#ff0000");
|
||||
let v = theme_variable(&mut s, "Brand", "#ff0000");
|
||||
for id in [a, b] {
|
||||
run(&mut s, "variable.bind", json!({"id":id,"field":"fill","variable":v}));
|
||||
}
|
||||
let red = group(&s, &[a, b], "FF0000");
|
||||
assert_eq!(red.targets.len(), 2);
|
||||
run(&mut s, "selection.createColorStyle", json!({"targets":red.targets,"name":"Brand"}));
|
||||
let style = s.doc().unwrap().style("Brand").unwrap();
|
||||
assert_eq!(style.bound.get("fills/0/color").map(String::as_str), v.as_str());
|
||||
run(&mut s, "variable.set", json!({"variable":v,"value":"#00ff00"}));
|
||||
assert_eq!(fill(&s, a), "00FF00");
|
||||
assert_eq!(fill(&s, b), "00FF00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_style_created_from_a_gradient_keeps_its_stop_variables() {
|
||||
use figcraft_color::{Color, Gradient, GradientKind, PaintKind};
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let paint = Paint::gradient(Gradient::default_for(GradientKind::Linear, Color::BLACK));
|
||||
run(&mut s, "fill.set", json!({"id":a,"paint":paint}));
|
||||
let v = theme_variable(&mut s, "Brand", "#ff0000");
|
||||
run(&mut s, "variable.bind", json!({"id":a,"field":"fills/0/stops/0/color","variable":v}));
|
||||
let row = collect(s.doc().unwrap(), &[a]).into_iter().next().unwrap();
|
||||
run(&mut s, "selection.createColorStyle", json!({"targets":row.targets,"name":"Shade"}));
|
||||
assert!(s.doc().unwrap().style("Shade").unwrap().bound.contains_key("fills/0/stops/0/color"));
|
||||
run(&mut s, "variable.set", json!({"variable":v,"value":"#00ff00"}));
|
||||
let PaintKind::Gradient(g) = &s.doc().unwrap().node(a).unwrap().fills[0].kind else { panic!("not a gradient") };
|
||||
assert_eq!(g.stops[0].color.to_hex(), "00FF00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_a_style_refuses_targets_bound_to_different_variables() {
|
||||
let mut s = Session::new();
|
||||
let a = rectangle(&mut s, "#ff0000");
|
||||
let b = rectangle(&mut s, "#ff0000");
|
||||
let v = theme_variable(&mut s, "Brand", "#ff0000");
|
||||
run(&mut s, "variable.bind", json!({"id":a,"field":"fill","variable":v}));
|
||||
let targets: Vec<_> = collect(s.doc().unwrap(), &[a, b]).into_iter().flat_map(|g| g.targets).collect();
|
||||
assert_eq!(targets.len(), 2);
|
||||
assert!(selection_colors::create_refusal(s.doc().unwrap(), &targets).is_some());
|
||||
let before = s.undo_depth();
|
||||
let styles = s.doc().unwrap().styles.len();
|
||||
let err = s.execute("selection.createColorStyle", &json!({"targets":targets,"name":"Red"})).unwrap_err();
|
||||
assert!(err.to_string().contains("different variables"), "{err}");
|
||||
assert_eq!(s.undo_depth(), before);
|
||||
let doc = s.doc().unwrap();
|
||||
assert_eq!(doc.styles.len(), styles);
|
||||
assert!(!doc.node(a).unwrap().bound.is_empty());
|
||||
assert!(doc.node(b).unwrap().styles.fill.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linked_row_switches_to_another_style() {
|
||||
let mut s = Session::new();
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ interaction feel not yet compared.
|
||||
| Transform box at small sizes | done | a press inside a layer smaller than its handles moves it, the nearest handle wins, handles answer just outside and are hidden under 16 px on screen; cursors follow each handle and the box's rotation |
|
||||
| Locked layers | done | selectable from the layers panel; no handles, not dragged with the rest of a selection |
|
||||
| Smart selection spacing | partial | with layers in a row, a column or an aligned grid selected, the Layout section shows horizontal and vertical spacing ("Mixed" until set; typed, scrubbed, fractional and negative) and pink handles in the gaps on the canvas drag them, one undo step, Escape cancels (`arrange.spacing`; sizes, parents and offsets within each track kept; at most 2,000 layers; flow children, locked layers and layers inside instances left out). A ring at each layer's centre drags it to another place in a regular row, column or complete top-left-aligned grid: the others close up and make room around it with the gaps kept, and with Cmd/Ctrl held the two layers swap places instead, previewed live while dragging (`selection_reorder_tests`; no ring on a layer under 16 px on screen). Not covered: reordering irregular grids, and several marked layers moved together. Checked by its author on macOS and in the app on Windows (#402) |
|
||||
| Selection colors | partial | for a mixed selection the Design panel lists their solid and gradient paints (children and text ranges included; hidden paints, masks, images, patterns and boolean operands left out; styles and variables listed apart): editing a colour changes every matching fill and stroke in one undo step, a row selects the layers that use it or applies a color style or color variable of the document, and a row linked to a style or variable detaches it, keeping the color (`selection.colors`, `selection.setPaint`, `selection.applyColorStyle`, `selection.bindColorVariable`, `selection.detachColor`, `selection.selectColor`). A row's Styles menu also lists each enabled library's published color styles under the library's name (a library that can't be read offers none), and a linked row's menu switches it to another style; a library style is copied in, linked and read-only, in the same undo step (`selection.applyColorStyle` with `library`). Create style on an ordinary row asks for a name (the next free "Color n" first) and makes a style of the row's whole paint, gradients, opacity and blend mode included, linked to every fill and stroke the row stands for in one undo step; a name in use keeps the window open (`selection.createColorStyle`). Text ranges with fills of their own take the style's value, without a link. No library variables from there, and no new variable from a row (#403) |
|
||||
| Selection colors | partial | for a mixed selection the Design panel lists their solid and gradient paints (children and text ranges included; hidden paints, masks, images, patterns and boolean operands left out; styles and variables listed apart): editing a colour changes every matching fill and stroke in one undo step, a row selects the layers that use it or applies a color style or color variable of the document, and a row linked to a style or variable detaches it, keeping the color (`selection.colors`, `selection.setPaint`, `selection.applyColorStyle`, `selection.bindColorVariable`, `selection.detachColor`, `selection.selectColor`). A row's Styles menu also lists each enabled library's published color styles under the library's name (a library that can't be read offers none), and a linked row's menu switches it to another style; a library style is copied in, linked and read-only, in the same undo step (`selection.applyColorStyle` with `library`). Create style on an ordinary row asks for a name (the next free "Color n" first) and makes a style of the row's whole paint, gradients, opacity, blend mode and variables bound to gradient stops included, linked to every fill and stroke the row stands for in one undo step; a name in use keeps the window open (`selection.createColorStyle`). Text ranges with fills of their own take the style's value, without a link. No library variables from there, and no new variable from a row (#403) |
|
||||
| Keyboard box selection | done | Alt+Space on macOS, Ctrl+Space on Windows and Linux, or F8 (`select.area`): a box around the selection or in the middle of the view; arrows move it, Cmd/Ctrl+arrows or Shift+arrows size it (Shift with Cmd/Ctrl: farther), Enter selects like a marquee (Shift+Enter toggles, Cmd/Ctrl+Enter nested), Escape closes |
|
||||
|
||||
## Tools
|
||||
|
||||
Reference in new issue
Block a user