mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 21:36:46 -06:00
M3: files from before the modes override keep the modes set inside their instances
A file or library saved before "modes" was an override group has a mode chosen on a layer inside an instance without its marker, so the next rebuild or library import still put the main's mode back. Copies follow their main layer's modes when the main changes them, so a layer inside an instance on other modes chose them itself. mark_layer_modes marks those when a file opens and when a published library is read from a file or a server. Layers on their main's modes and top-level instances stay unmarked.
This commit is contained in:
@@ -116,6 +116,23 @@ pub fn inside_instance(doc: &Document, id: NodeId) -> bool {
|
||||
enclosing_instance(doc, id).is_some()
|
||||
}
|
||||
|
||||
/// Mark the `modes` override on layers inside instances whose variable modes differ from their
|
||||
/// main layer's, for files from before `modes` was an override group. Copies follow their main
|
||||
/// layer's modes, so a layer on other modes chose them itself.
|
||||
pub fn mark_layer_modes(doc: &mut Document) {
|
||||
let chose: Vec<NodeId> = doc
|
||||
.nodes()
|
||||
.filter(|n| !n.overrides.contains("modes") && n.source.and_then(|s| doc.node(s)).is_some_and(|m| m.var_modes != n.var_modes))
|
||||
.map(|n| n.id)
|
||||
.filter(|id| inside_instance(doc, *id))
|
||||
.collect();
|
||||
for id in chose {
|
||||
if let Some(n) = doc.node_mut(id) {
|
||||
n.overrides.insert("modes".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The slot property frame `id` stands for (its name and preferred components): a frame bound to
|
||||
/// a slot property of its component, or an instance's copy of one (followed through its sources).
|
||||
pub fn slot_def(doc: &Document, id: NodeId) -> Option<(&str, &[NodeId])> {
|
||||
|
||||
@@ -175,3 +175,22 @@ fn a_mode_set_on_an_icon_inside_a_placed_instance_survives_main_edits() {
|
||||
run(&mut s, "component.resetInstance", json!({"ids": [placed]}));
|
||||
assert_eq!(fill(&s, icon), "FF0000", "reset all brings back the main's mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_from_before_modes_were_overrides_keeps_the_modes_chosen_inside_its_instances() {
|
||||
let mut s = Session::new();
|
||||
let (button, _, nested) = modal(&mut s);
|
||||
// Saved before `modes` was an override group: the copy has its mode but no marker.
|
||||
s.doc_mut().unwrap().node_mut(nested).unwrap().overrides.remove("modes");
|
||||
let path = std::env::temp_dir().join(format!("figcraft-instance-modes-{}.figcraft", std::process::id()));
|
||||
let p = path.to_string_lossy().into_owned();
|
||||
run(&mut s, "file.saveAs", json!({"path": p}));
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.open", json!({"path": p}));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let doc = s.doc().unwrap();
|
||||
assert!(doc.node(nested).unwrap().overrides.contains("modes"), "opening it marks the mode the copy chose");
|
||||
assert!(doc.nodes().filter(|n| n.id != nested).all(|n| !n.overrides.contains("modes")), "layers on their main's modes stay unmarked");
|
||||
run(&mut s, "node.set", json!({"ids": [button], "layout": "horizontal"}));
|
||||
assert_eq!(fill(&s, nested), "0000FF", "a rebuild keeps it");
|
||||
}
|
||||
|
||||
@@ -413,6 +413,7 @@ impl DocState {
|
||||
}
|
||||
doc.link_layer_text_styles();
|
||||
cmd::styles::apply_paint_bindings_on_load(&mut doc);
|
||||
components::mark_layer_modes(&mut doc);
|
||||
// Initial import settlement belongs to the saved baseline, not the user's first edit.
|
||||
let page = doc.current_page.filter(|p| doc.pages.contains(p)).or_else(|| doc.first_page()).unwrap_or(NodeId(0));
|
||||
let doc = Arc::new(doc);
|
||||
|
||||
@@ -553,11 +553,18 @@ fn stat(_path: &str) -> std::result::Result<Stamp, String> {
|
||||
Err(NO_FILES.into())
|
||||
}
|
||||
|
||||
/// A library read from a file or a server, with the modes chosen inside its instances marked when
|
||||
/// it was published before they were overrides ([`crate::components::mark_layer_modes`]).
|
||||
pub(crate) fn settled(mut lib: PublishedLibrary) -> PublishedLibrary {
|
||||
crate::components::mark_layer_modes(Arc::make_mut(&mut lib.doc));
|
||||
lib
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn read_file(path: &str) -> std::result::Result<PublishedLibrary, String> {
|
||||
let f = crate::file_access::open(path).map_err(|e| e.to_string())?;
|
||||
match figcraft_format::read_library(std::io::BufReader::new(f)) {
|
||||
Ok(Some(lib)) => Ok(lib),
|
||||
Ok(Some(lib)) => Ok(settled(lib)),
|
||||
Ok(None) => Err(format!("{path} has no published library: open it and publish it first (Libraries ▸ Publish library…)")),
|
||||
Err(e) => Err(format!("{path}: {e}")),
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn archive(lib: &PublishedLibrary) -> Result<Vec<u8>> {
|
||||
/// The library in an archive from a server (untrusted: read with the library file's limits).
|
||||
pub fn read_archive(bytes: &[u8]) -> std::result::Result<PublishedLibrary, String> {
|
||||
match figcraft_format::read_library(Cursor::new(bytes)) {
|
||||
Ok(Some(lib)) => Ok(lib),
|
||||
Ok(Some(lib)) => Ok(crate::library::settled(lib)),
|
||||
Ok(None) => Err("the server sent a file without a library".into()),
|
||||
Err(e) => Err(format!("the server sent a library that can't be read ({e})")),
|
||||
}
|
||||
|
||||
@@ -345,32 +345,42 @@ fn library_style_bindings_ignore_unrelated_local_variables() {
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
/// A library "Modal" holding a Button instance whose icon copy is on mode Dark: (the Button main,
|
||||
/// the icon copy in Modal).
|
||||
fn nested_mode_kit(lib: &mut Session) -> (NodeId, NodeId) {
|
||||
theme(lib);
|
||||
let f = idv(&ex(lib, "frame.create", json!({"x": 0, "y": 0, "width": 20, "height": 20, "name": "Icon"})));
|
||||
ex(lib, "variable.bind", json!({"ids": [f], "field": "fill", "variable": "primary"}));
|
||||
let icon = idv(&ex(lib, "component.create", json!({"ids": [f]})));
|
||||
let b = idv(&ex(lib, "frame.create", json!({"x": 100, "y": 0, "width": 60, "height": 40, "name": "Button"})));
|
||||
ex(lib, "instance.create", json!({"component": icon, "x": 10, "y": 10, "parent": b}));
|
||||
let button = idv(&ex(lib, "component.create", json!({"ids": [b]})));
|
||||
let m = idv(&ex(lib, "frame.create", json!({"x": 300, "y": 0, "width": 200, "height": 100, "name": "Modal"})));
|
||||
let close = idv(&ex(lib, "instance.create", json!({"component": button, "x": 10, "y": 10, "parent": m})));
|
||||
let nested = lib.doc().unwrap().node(close).unwrap().children[0];
|
||||
ex(lib, "variable.setMode", json!({"ids": [nested], "collection": "Theme", "mode": "Dark"}));
|
||||
ex(lib, "component.create", json!({"ids": [m]}));
|
||||
(button, nested)
|
||||
}
|
||||
|
||||
/// The icon inside the Button inside library Modal instance `inst`.
|
||||
fn modal_icon(s: &Session, inst: NodeId) -> NodeId {
|
||||
let doc = s.doc().unwrap();
|
||||
doc.node(doc.node(inst).unwrap().children[0]).unwrap().children[0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modes_set_inside_nested_instances_come_along() {
|
||||
let d = dir("nested-modes");
|
||||
let lib_path = path_str(&d.join("kit.figcraft"));
|
||||
let mut lib = Session::new();
|
||||
theme(&mut lib);
|
||||
let f = idv(&ex(&mut lib, "frame.create", json!({"x": 0, "y": 0, "width": 20, "height": 20, "name": "Icon"})));
|
||||
ex(&mut lib, "variable.bind", json!({"ids": [f], "field": "fill", "variable": "primary"}));
|
||||
let icon = idv(&ex(&mut lib, "component.create", json!({"ids": [f]})));
|
||||
let b = idv(&ex(&mut lib, "frame.create", json!({"x": 100, "y": 0, "width": 60, "height": 40, "name": "Button"})));
|
||||
ex(&mut lib, "instance.create", json!({"component": icon, "x": 10, "y": 10, "parent": b}));
|
||||
let button = idv(&ex(&mut lib, "component.create", json!({"ids": [b]})));
|
||||
let m = idv(&ex(&mut lib, "frame.create", json!({"x": 300, "y": 0, "width": 200, "height": 100, "name": "Modal"})));
|
||||
let close = idv(&ex(&mut lib, "instance.create", json!({"component": button, "x": 10, "y": 10, "parent": m})));
|
||||
let nested = lib.doc().unwrap().node(close).unwrap().children[0];
|
||||
ex(&mut lib, "variable.setMode", json!({"ids": [nested], "collection": "Theme", "mode": "Dark"}));
|
||||
ex(&mut lib, "component.create", json!({"ids": [m]}));
|
||||
let (button, _) = nested_mode_kit(&mut lib);
|
||||
let lid = publish_to(&mut lib, &lib_path);
|
||||
|
||||
let mut s = Session::new();
|
||||
ex(&mut s, "library.add", json!({"path": lib_path}));
|
||||
let inst = idv(&ex(&mut s, "library.place", json!({"library": lid, "component": "Modal", "x": 0, "y": 0})));
|
||||
let copy = |s: &Session| {
|
||||
let doc = s.doc().unwrap();
|
||||
doc.node(doc.node(inst).unwrap().children[0]).unwrap().children[0]
|
||||
};
|
||||
let copy = |s: &Session| modal_icon(s, inst);
|
||||
assert_eq!(fill(&s, copy(&s)), "FFFFFF", "the Dark mode set on the nested icon comes along");
|
||||
// An update that rebuilds the Button copies keeps it too.
|
||||
ex(&mut lib, "node.set", json!({"ids": [button], "layout": "horizontal"}));
|
||||
@@ -380,3 +390,21 @@ fn modes_set_inside_nested_instances_come_along() {
|
||||
healthy(&s);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_library_published_before_modes_were_overrides_keeps_the_modes_set_inside_nested_instances() {
|
||||
let d = dir("nested-modes-old");
|
||||
let lib_path = path_str(&d.join("kit.figcraft"));
|
||||
let mut lib = Session::new();
|
||||
let (_, nested) = nested_mode_kit(&mut lib);
|
||||
// Published before `modes` was an override group: the copy has its mode but no marker.
|
||||
lib.doc_mut().unwrap().node_mut(nested).unwrap().overrides.remove("modes");
|
||||
let lid = publish_to(&mut lib, &lib_path);
|
||||
|
||||
let mut s = Session::new();
|
||||
ex(&mut s, "library.add", json!({"path": lib_path}));
|
||||
let inst = idv(&ex(&mut s, "library.place", json!({"library": lid, "component": "Modal", "x": 0, "y": 0})));
|
||||
assert_eq!(fill(&s, modal_icon(&s, inst)), "FFFFFF", "the Dark mode comes along");
|
||||
healthy(&s);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user