mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 13:26:54 -06:00
Merge main into feat/widget-input
# Conflicts: # ROADMAP.md # crates/plugin/src/manifest.rs # crates/ui-egui/src/canvas.rs # docs/gaps.md # docs/plugins.md # docs/roadmap.md # docs/target-app-parity.md
This commit is contained in:
Generated
+7
@@ -9,6 +9,13 @@ dependencies = [
|
||||
"figcraft-plugin-sdk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "export-names"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"figcraft-plugin-sdk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "figcraft-plugin-sdk"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "export-names"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
figcraft-plugin-sdk = { workspace = true }
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "org.figcraft.examples.export-names",
|
||||
"name": "Export names",
|
||||
"version": "1.0.0",
|
||||
"description": "Saves the names of the selected layers as a text file, one per line.",
|
||||
"main": "export_names.wasm",
|
||||
"api": 4,
|
||||
"permissions": {
|
||||
"access": "read",
|
||||
"commands": ["node.get"],
|
||||
"saveFile": true
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"id": "export",
|
||||
"label": "Export names…",
|
||||
"params": [
|
||||
{ "name": "children", "label": "Include layers inside", "type": "boolean", "default": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Export names: saves the names of the selected layers (optionally the layers inside them too) as
|
||||
//! the text file `layer-names.txt`, through the host's own Save dialog (`save_file`).
|
||||
|
||||
use figcraft_plugin_sdk::{Input, Result, Value, call, entry, json, notify, save_file};
|
||||
|
||||
/// Layers looked at in one run, at most.
|
||||
const MAX_LAYERS: usize = 10_000;
|
||||
|
||||
/// The names as the file's text: one per line, a line break at the end, line breaks inside a name
|
||||
/// turned into spaces.
|
||||
fn names_text(names: &[String]) -> String {
|
||||
let mut text = String::new();
|
||||
for name in names {
|
||||
text.extend(name.chars().map(|c| if c == '\n' || c == '\r' { ' ' } else { c }));
|
||||
text.push('\n');
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn run(input: Input) -> Result<Value> {
|
||||
if input.selection.is_empty() {
|
||||
return Err("Select the layers to export first".into());
|
||||
}
|
||||
let inside = input.bool("children").unwrap_or(true);
|
||||
// Depth first, in layer order: the stack takes the children reversed.
|
||||
let mut stack: Vec<u64> = input.selection.iter().rev().copied().collect();
|
||||
let mut names = Vec::new();
|
||||
while let Some(id) = stack.pop() {
|
||||
if names.len() >= MAX_LAYERS {
|
||||
break;
|
||||
}
|
||||
let node = call("node.get", &json!({"id": id}))?;
|
||||
names.push(node["name"].as_str().unwrap_or_default().to_string());
|
||||
if inside && let Some(children) = node["children"].as_array() {
|
||||
stack.extend(children.iter().rev().filter_map(|c| c["id"].as_u64()));
|
||||
}
|
||||
}
|
||||
save_file("layer-names.txt", "text/plain", names_text(&names).as_bytes())?;
|
||||
notify(&format!("Exporting {} names", names.len()));
|
||||
Ok(json!({"names": names.len()}))
|
||||
}
|
||||
|
||||
entry!(run);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::names_text;
|
||||
|
||||
#[test]
|
||||
fn names_become_lines() {
|
||||
let names = ["Card".to_string(), "Title\nline".to_string(), String::new()];
|
||||
assert_eq!(names_text(&names), "Card\nTitle line\n\n");
|
||||
assert_eq!(names_text(&[]), "");
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,14 @@ pub fn delete_setting(key: &str) {
|
||||
host::setting_delete(key.as_bytes());
|
||||
}
|
||||
|
||||
/// Offer a file to save: a suggested file `name`, its MIME type and the bytes. FigCraft asks the
|
||||
/// user where it goes (a download in the browser) once the run succeeds; the plugin never learns
|
||||
/// a path. Needs `"api": 4` and `permissions.saveFile` in the manifest. A refusal (no permission,
|
||||
/// a bad name, too much data, a second file in one run) is an error.
|
||||
pub fn save_file(name: &str, mime: &str, data: &[u8]) -> Result<()> {
|
||||
if host::save_file(name.as_bytes(), mime.as_bytes(), data) == 0 { Ok(()) } else { Err(String::from_utf8_lossy(&host::reply()).into_owned()) }
|
||||
}
|
||||
|
||||
/// The body of `figcraft_run` (use [`entry!`]): read the input, run `f`, hand back its output.
|
||||
pub fn run_plugin(input_len: i32, f: fn(Input) -> Result<Value>) -> i32 {
|
||||
let input = host::reply();
|
||||
@@ -256,6 +264,7 @@ mod host {
|
||||
pub fn setting_set(key: *const u8, key_len: usize, value: *const u8, value_len: usize) -> i32;
|
||||
pub fn setting_delete(key: *const u8, key_len: usize);
|
||||
pub fn widget_render(ptr: *const u8, len: usize) -> i32;
|
||||
pub fn save_file(name: *const u8, name_len: usize, mime: *const u8, mime_len: usize, data: *const u8, data_len: usize) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +310,9 @@ mod host {
|
||||
pub fn widget_render(s: &[u8]) -> i32 {
|
||||
unsafe { sys::widget_render(s.as_ptr(), s.len()) }
|
||||
}
|
||||
pub fn save_file(name: &[u8], mime: &[u8], data: &[u8]) -> i32 {
|
||||
unsafe { sys::save_file(name.as_ptr(), name.len(), mime.as_ptr(), mime.len(), data.as_ptr(), data.len()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Outside WebAssembly (a plugin's own unit tests) every host call fails.
|
||||
@@ -334,6 +346,9 @@ mod host {
|
||||
pub fn widget_render(_s: &[u8]) -> i32 {
|
||||
1
|
||||
}
|
||||
pub fn save_file(_name: &[u8], _mime: &[u8], _data: &[u8]) -> i32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -357,6 +372,7 @@ mod tests {
|
||||
assert_eq!((e.control.as_str(), &e.value, &e.values), ("go", &Value::Null, &json!({"n": 2})));
|
||||
assert!(show_ui(&json!({})).is_err() && setting("k").is_none() && set_setting("k", &json!(1)).is_err());
|
||||
assert!(render_widget(&json!({})).is_err() && i.widget.is_none());
|
||||
assert!(save_file("a.txt", "text/plain", b"x").is_err());
|
||||
let w = Input::parse(r#"{"command":"widget","widget":{"id":9,"state":{"n":1},"event":{"target":"inc"}}}"#)
|
||||
.unwrap_or_default()
|
||||
.widget
|
||||
|
||||
Reference in New Issue
Block a user