mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-11 12:55:16 +00:00
Ruler guides can belong to a frame: `guide.add/move/remove/clear` take `frame` (as well as `page`), keeping the guide in the frame's own coordinates. Frame guides move with the frame, span only it on the canvas, are saved and copied with it, and snap the layers inside it. A guide dragged out of a ruler and dropped on a frame becomes that frame's guide; its label shows the offset in the frame. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.5 KiB
Rust
164 lines
6.5 KiB
Rust
//! Pages.
|
|
|
|
use figcraft_doc::*;
|
|
use serde_json::{Value, json};
|
|
|
|
use super::{CommandSpec, ok};
|
|
use crate::params::*;
|
|
use crate::{Session, bad, spec};
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
spec!("page.add", "Add page", "{name?}", true, |s, p| {
|
|
let doc = s.doc_mut()?;
|
|
let name = string(p, "name").unwrap_or_else(|| format!("Page {}", doc.pages.len() + 1));
|
|
let id = doc.add_page(&name);
|
|
let ds = s.ds_mut()?;
|
|
ds.page = id;
|
|
ds.selection.clear();
|
|
ds.zoom_to_fit();
|
|
Ok(json!({"id": id}))
|
|
}),
|
|
spec!("page.select", "Go to page", "{id | index}", false, |s, p| {
|
|
let doc = s.doc()?;
|
|
let id = match id(p, "id") {
|
|
Some(i) => i,
|
|
None => {
|
|
let k = req_num("page.select", p, "index")? as usize;
|
|
*doc.pages.get(k).ok_or_else(|| bad("page.select", "no such page"))?
|
|
}
|
|
};
|
|
if !doc.pages.contains(&id) {
|
|
return Err(bad("page.select", format!("{id} is not a page")));
|
|
}
|
|
let ds = s.ds_mut()?;
|
|
ds.page = id;
|
|
ds.selection.clear();
|
|
ds.text_edit = None;
|
|
ds.zoom_to_fit();
|
|
ok()
|
|
}),
|
|
spec!("page.rename", "Rename page", "{id?, name}", true, |s, p| {
|
|
let name = req_string("page.rename", p, "name")?;
|
|
let id = id(p, "id").unwrap_or(s.page()?);
|
|
s.doc_mut()?.get_mut(id)?.name = name;
|
|
ok()
|
|
}),
|
|
spec!("page.delete", "Delete page", "{id?}", true, |s, p| {
|
|
let id = id(p, "id").unwrap_or(s.page()?);
|
|
let doc = s.doc_mut()?;
|
|
doc.remove(id)?;
|
|
let first = doc.first_page();
|
|
let ds = s.ds_mut()?;
|
|
if ds.page == id
|
|
&& let Some(f) = first
|
|
{
|
|
ds.page = f;
|
|
ds.selection.clear();
|
|
}
|
|
ok()
|
|
}),
|
|
spec!("page.duplicate", "Duplicate page", "{id?}", true, |s, p| {
|
|
let src = id(p, "id").unwrap_or(s.page()?);
|
|
let doc = s.doc_mut()?;
|
|
let name = format!("{} (Copy)", doc.get(src)?.name);
|
|
let np = doc.add_page(&name);
|
|
if let NodeKind::Page { background, .. } = doc.get(src)?.kind.clone()
|
|
&& let NodeKind::Page { background: b, .. } = &mut doc.get_mut(np)?.kind
|
|
{
|
|
*b = background;
|
|
}
|
|
for c in doc.get(src)?.children.clone() {
|
|
doc.duplicate_into(c, np, None)?;
|
|
}
|
|
// Keep the new page right after the source.
|
|
if let Some(si) = doc.pages.iter().position(|x| *x == src) {
|
|
doc.pages.retain(|x| *x != np);
|
|
doc.pages.insert(si + 1, np);
|
|
}
|
|
s.ds_mut()?.page = np;
|
|
Ok(json!({"id": np}))
|
|
}),
|
|
spec!("page.move", "Move page", "{id?, index}", true, |s, p| {
|
|
let id = id(p, "id").unwrap_or(s.page()?);
|
|
let k = req_num("page.move", p, "index")?.max(0.0) as usize;
|
|
let doc = s.doc_mut()?;
|
|
doc.pages.retain(|x| *x != id);
|
|
let k = k.min(doc.pages.len());
|
|
doc.pages.insert(k, id);
|
|
ok()
|
|
}),
|
|
spec!("page.setBackground", "Page background", "{id?, color}", true, |s, p| {
|
|
let c = color(p, "color").ok_or_else(|| bad("page.setBackground", "`color` required"))?;
|
|
let id = id(p, "id").unwrap_or(s.page()?);
|
|
if let NodeKind::Page { background, .. } = &mut s.doc_mut()?.get_mut(id)?.kind {
|
|
*background = c;
|
|
}
|
|
ok()
|
|
}),
|
|
spec!("page.list", "List pages", "{}", false, |s, _| {
|
|
let doc = s.doc()?;
|
|
let cur = s.page()?;
|
|
Ok(Value::Array(
|
|
doc.pages
|
|
.iter()
|
|
.filter_map(|p| doc.node(*p))
|
|
.map(|n| json!({"id": n.id, "name": n.name, "current": n.id == cur, "children": n.children.len()}))
|
|
.collect(),
|
|
))
|
|
}),
|
|
spec!("guide.add", "Add guide", "{vertical (bool), offset, frame? | page?} → {index}", true, |s, p| {
|
|
let owner = guide_owner(s, "guide.add", p)?;
|
|
let offset = req_num("guide.add", p, "offset")?.clamp(-1e7, 1e7);
|
|
let g = Guide { vertical: boolean(p, "vertical").unwrap_or(true), offset };
|
|
let guides = s.doc_mut()?.get_mut(owner)?.guides_mut();
|
|
if guides.len() >= 10_000 {
|
|
return Err(bad("guide.add", "too many guides"));
|
|
}
|
|
guides.push(g);
|
|
Ok(json!({"index": guides.len() - 1}))
|
|
}),
|
|
spec!("guide.move", "Move guide", "{index, offset, frame? | page?}", true, |s, p| {
|
|
let owner = guide_owner(s, "guide.move", p)?;
|
|
let k = guide_index("guide.move", p)?;
|
|
let offset = req_num("guide.move", p, "offset")?.clamp(-1e7, 1e7);
|
|
s.doc_mut()?.get_mut(owner)?.guides_mut().get_mut(k).ok_or_else(|| bad("guide.move", "no such guide"))?.offset = offset;
|
|
ok()
|
|
}),
|
|
spec!("guide.remove", "Remove guide", "{index, frame? | page?}", true, |s, p| {
|
|
let owner = guide_owner(s, "guide.remove", p)?;
|
|
let k = guide_index("guide.remove", p)?;
|
|
let guides = s.doc_mut()?.get_mut(owner)?.guides_mut();
|
|
if k >= guides.len() {
|
|
return Err(bad("guide.remove", "no such guide"));
|
|
}
|
|
guides.remove(k);
|
|
ok()
|
|
}),
|
|
spec!("guide.clear", "Remove all guides", "{frame? | page?}", true, |s, p| {
|
|
let owner = guide_owner(s, "guide.clear", p)?;
|
|
s.doc_mut()?.get_mut(owner)?.guides_mut().clear();
|
|
ok()
|
|
}),
|
|
]
|
|
}
|
|
|
|
/// Whose guides a command edits: `frame` (in its own coordinates), else `page`, else the current
|
|
/// page.
|
|
fn guide_owner(s: &Session, cmd: &str, p: &Value) -> crate::Result<NodeId> {
|
|
let owner = match id(p, "frame") {
|
|
Some(f) => f,
|
|
None => id(p, "page").unwrap_or(s.page()?),
|
|
};
|
|
match s.doc()?.node(owner) {
|
|
Some(n) if n.is_page() || n.kind.is_frame_like() => Ok(owner),
|
|
Some(_) => Err(bad(cmd, "guides go on a page or a frame")),
|
|
None => Err(bad(cmd, "no such page or frame")),
|
|
}
|
|
}
|
|
|
|
/// A guide index from params (never negative).
|
|
fn guide_index(cmd: &str, p: &Value) -> crate::Result<usize> {
|
|
req_index(cmd, p, "index")
|
|
}
|