Files
Marcos Paulo 53b8dc470b feat: rebrand to PaperCraft; variable modes, lint_design, Design tab, icon, docs and skills
Product, crate and binaries are now PaperCraft/papercraft (papercraft-cli, papercraft-mcp);
projects, icon sets and theme in the old paper-rs locations are still read and .paper-rs files
still open. set_variables {mode} and set_variable_mode switch whole themes; lint_design checks
contrast, type scale, 4px grid, touch targets, layer names and hard-coded colors; get_variables
exports css/json tokens; the sidebar gets a Design tab for variables, modes and components.
New app icon, README, docs, ROADMAP and agent skills.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:08:55 -03:00

205 lines
6.7 KiB
Rust

//! Milestone 5: layers drag-reorder, command palette, shortcuts, inspector.
mod common;
use common::*;
use papercraft::editor::shortcuts::{self, Action};
use papercraft::editor::{Button, Editor, InputEvent, Key, Modifiers};
use papercraft::geom::{Rect, v2};
use papercraft::model::{Color, Node, NodeId, NodeKind};
use papercraft::render::frame;
use papercraft::ui::palette;
use papercraft::ui::{LayerDrop, UiState, drop_layer};
fn rect(e: &mut Editor, r: Rect, c: Color, parent: Option<NodeId>) -> NodeId {
let mut n = Node::new(NodeId(0), NodeKind::Rect, r);
n.style.set_fill(Some(c));
e.add_node(n, parent)
}
#[test]
fn layers_drop_above_below_and_into() {
let mut e = editor(800.0, 600.0);
let a = rect(&mut e, Rect::new(0.0, 0.0, 10.0, 10.0), Color::BLACK, None);
let b = rect(&mut e, Rect::new(20.0, 0.0, 10.0, 10.0), Color::BLACK, None);
let c = rect(&mut e, Rect::new(40.0, 0.0, 10.0, 10.0), Color::BLACK, None);
let f = e.add_node(
Node::new(
NodeId(0),
NodeKind::Frame { clip: true },
Rect::new(100.0, 100.0, 200.0, 200.0),
),
None,
);
assert_eq!(e.doc.root, vec![a, b, c, f]);
// The panel lists front-most first; "above c" means in front of c.
drop_layer(&mut e, a, LayerDrop::Above(c));
assert_eq!(e.doc.root, vec![b, c, a, f]);
drop_layer(&mut e, a, LayerDrop::Below(b));
assert_eq!(e.doc.root, vec![a, b, c, f]);
// Into a frame keeps the canvas position.
let before = e.doc.world_rect(b).unwrap();
drop_layer(&mut e, b, LayerDrop::Into(f));
assert_eq!(e.doc.get(b).unwrap().parent, Some(f));
assert_eq!(e.doc.world_rect(b).unwrap(), before);
// A frame can't go into itself or its children.
drop_layer(&mut e, f, LayerDrop::Into(f));
assert_eq!(e.doc.get(f).unwrap().parent, None);
e.undo();
assert_eq!(e.doc.get(b).unwrap().parent, None);
e.doc.validate().unwrap();
}
#[test]
fn command_palette_finds_and_runs() {
let mut e = papercraft::demo::showcase();
let mut st = UiState::default();
let r = palette::search(&e, &st, "zoom fit");
assert!(
matches!(r[0].cmd, palette::Command::Run(Action::ZoomFit)),
"{}",
r[0].label
);
// ⌘K opens the palette through the editor's UI events.
key(&mut e, Key::Char('k'), Modifiers::cmd());
assert!(matches!(
e.ui_events.as_slice(),
[papercraft::editor::commands::UiEvent::TogglePalette]
));
// Run "Select layer: Week plan".
let r = palette::search(&e, &st, "select layer week");
let cmd = r[0].cmd.clone();
palette::run(&mut e, &mut st, cmd);
assert_eq!(e.doc.get(e.selection[0]).unwrap().name, "Week plan");
}
#[test]
fn editing_shortcuts() {
let mut e = editor(800.0, 600.0);
let a = rect(
&mut e,
Rect::new(100.0, 100.0, 50.0, 50.0),
Color::rgb(200, 0, 0),
None,
);
let b = rect(
&mut e,
Rect::new(300.0, 100.0, 50.0, 50.0),
Color::rgb(0, 0, 200),
None,
);
// Tab cycles siblings.
e.selection = vec![b];
key(&mut e, Key::Tab, Modifiers::NONE);
assert_eq!(e.selection, vec![a]);
key(&mut e, Key::Tab, Modifiers::SHIFT);
assert_eq!(e.selection, vec![b]);
// Copy / paste properties.
e.selection = vec![a];
key(
&mut e,
Key::Char('c'),
Modifiers {
alt: true,
..Modifiers::cmd()
},
);
e.selection = vec![b];
key(
&mut e,
Key::Char('v'),
Modifiers {
alt: true,
..Modifiers::cmd()
},
);
assert_eq!(
e.doc.get(b).unwrap().style.fill(),
Some(Color::rgb(200, 0, 0))
);
// Alt-drag duplicates (one undo step), leaving the original.
e.selection = vec![a];
e.handle(InputEvent::Modifiers(Modifiers {
alt: true,
..Modifiers::NONE
}));
ev_drag(&mut e, v2(120.0, 120.0), v2(120.0, 320.0), 0.0, true);
e.handle(InputEvent::Modifiers(Modifiers::NONE));
assert_eq!(e.doc.nodes.len(), 3);
assert_eq!(e.doc.world_rect(a).unwrap().y, 100.0);
assert_eq!(e.doc.world_rect(e.selection[0]).unwrap().y, 300.0);
e.undo();
assert_eq!(e.doc.nodes.len(), 2);
// Flip a vector path horizontally.
let mut p = Node::new(
NodeId(0),
NodeKind::Line {
a: v2(0.0, 0.0),
b: v2(40.0, 20.0),
},
Rect::new(500.0, 100.0, 40.0, 20.0),
);
p.style.stroke = Some(Color::BLACK);
let l = e.add_node(p, None);
e.selection = vec![l];
key(&mut e, Key::Char('h'), Modifiers::SHIFT);
let NodeKind::Line { a: la, b: lb } = e.doc.get(l).unwrap().kind else {
unreachable!()
};
assert_eq!((la, lb), (v2(40.0, 0.0), v2(0.0, 20.0)));
// Pages: PageDown/PageUp.
e.add_page();
key(&mut e, Key::PageUp, Modifiers::NONE);
assert_eq!(e.active_page, 0);
key(&mut e, Key::PageDown, Modifiers::NONE);
assert_eq!(e.active_page, 1);
// Rename and hide-UI become UI events.
e.switch_page(0);
e.selection = vec![a];
key(&mut e, Key::Char('r'), Modifiers::cmd());
key(&mut e, Key::Char('\\'), Modifiers::cmd());
use papercraft::editor::commands::UiEvent;
assert_eq!(e.ui_events, vec![UiEvent::Rename(a), UiEvent::ToggleUi]);
let _ = Button::Primary;
}
#[test]
fn shortcut_table_is_complete_and_conflict_free() {
// Every action reachable from the menus has a key, and help groups every binding.
let sections = shortcuts::help_sections();
let total: usize = sections.iter().map(|(_, r)| r.len()).sum();
assert_eq!(total, shortcuts::help_table().len());
assert!(total > 90, "{total} shortcuts");
}
#[test]
fn m5_palette_and_shortcuts_screenshots() {
let Some(g) = gpu() else { return };
let mut e = papercraft::demo::showcase();
let mut st = UiState {
project_name: "Launch plan".into(),
save_state: "Saved".into(),
..Default::default()
};
st.palette.toggle();
st.palette.query = "al".into();
let img = frame::capture(g, &mut e, &mut st, [1440.0, 900.0], 2.0, |ed| {
ed.zoom_to_fit()
});
img.save(shot("milestone5_palette.png")).unwrap();
st.palette.open = false;
st.show_help = true;
let img = frame::capture(g, &mut e, &mut st, [1440.0, 900.0], 2.0, |ed| {
ed.zoom_to_fit()
});
img.save(shot("milestone5_shortcuts.png")).unwrap();
st.show_help = false;
// Inspector for a selected frame, layers tree expanded.
let mut e = papercraft::demo::showcase();
e.selection = vec![e.doc.root[0]];
let img = frame::capture(g, &mut e, &mut st, [1440.0, 900.0], 2.0, |ed| {
ed.zoom_to_fit()
});
img.save(shot("milestone5_inspector.png")).unwrap();
}