mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 03:16:53 -06:00
- Device rotation: phones and tablets turn to landscape (`proto.settings {rotation}`), saved in
the file and imported from REST files (`CCW_90`); the presenter and the preview pane size the
screen to the turned device and draw its body turned. A rotate toggle sits beside the device
picker in the Prototype panel.
- Presentation options: fit, fit width, fill and 100% (`proto.presentOptions {scale?,
deviceFrame?}`, an Options menu in the presenter, Z cycles them, R restarts), the device frame
shown or hidden, kept between presentations; what overflows the window pans by wheel or drag.
Presenter textures are capped to the GPU's limit (big frames at 100% no longer overflow it).
- Scroll momentum: drags fling and slow down exactly per frame-rate-independent sub-steps, the
wheel coasts after it stops, a drag past the ends stretches and springs back; repaints only
while moving, a press catches a fling.
- Variable modes in the Design panel: frames pick a mode per multi-mode collection, or Auto
(inherited); `variable.setMode {mode: null}` sets Auto.
- Tests in new modules (engine proto_present_tests, UI present_view_tests); docs and ROADMAP.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.6 KiB
Rust
100 lines
5.6 KiB
Rust
//! Prototype presentation settings through the session: the device's rotation (saved in the
|
|
//! file), the presenter's scale options and explicit variable modes set back to Auto.
|
|
|
|
use figcraft_doc::NodeKind;
|
|
use figcraft_doc::proto::{DeviceRotation, PrototypeSettings};
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::params::id_of;
|
|
|
|
fn settings(s: &Session) -> PrototypeSettings {
|
|
let doc = s.doc().unwrap();
|
|
match &doc.node(s.page().unwrap()).unwrap().kind {
|
|
NodeKind::Page { prototype, .. } => prototype.clone(),
|
|
_ => panic!("not a page"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn device_rotation_turns_phones_and_tablets_and_is_saved() {
|
|
let mut s = Session::new();
|
|
s.execute("proto.settings", &json!({"device": "iPhone 16"})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Portrait);
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((393.0, 852.0)));
|
|
// Landscape swaps the screen's sides.
|
|
s.execute("proto.settings", &json!({"rotation": "landscape"})).unwrap();
|
|
assert!(settings(&s).landscape());
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((852.0, 393.0)));
|
|
// Undoable, and degrees work too.
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Portrait);
|
|
s.execute("proto.settings", &json!({"rotation": 90})).unwrap();
|
|
assert!(settings(&s).landscape());
|
|
// Laptops and desktops don't turn: the rotation is kept but doesn't apply.
|
|
s.execute("proto.settings", &json!({"device": "MacBook Air"})).unwrap();
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Landscape);
|
|
assert!(!settings(&s).landscape());
|
|
assert_eq!(settings(&s).device().map(|d| (d.width, d.height)), Some((1280.0, 832.0)));
|
|
// Junk is refused and changes nothing.
|
|
for bad in [json!("sideways"), json!(45), json!(true), json!({}), json!(f64::MAX)] {
|
|
assert!(s.execute("proto.settings", &json!({"rotation": bad})).is_err(), "{bad}");
|
|
}
|
|
assert_eq!(settings(&s).rotation, DeviceRotation::Landscape);
|
|
// Saved in the file and read back; files without it open in portrait.
|
|
s.execute("proto.settings", &json!({"device": "iPad mini"})).unwrap();
|
|
let bytes = figcraft_format::save_bytes(s.doc().unwrap(), None).unwrap();
|
|
let (doc, _) = figcraft_format::load_bytes(&bytes).unwrap();
|
|
let NodeKind::Page { prototype, .. } = &doc.node(doc.pages[0]).unwrap().kind else { panic!("not a page") };
|
|
assert_eq!(prototype.device().map(|d| (d.width, d.height)), Some((1133.0, 744.0)));
|
|
let old: PrototypeSettings = serde_json::from_value(json!({"device": "iPhone 16"})).unwrap();
|
|
assert_eq!(old.rotation, DeviceRotation::Portrait);
|
|
}
|
|
|
|
#[test]
|
|
fn present_options_ask_the_ui_and_check_their_values() {
|
|
let mut s = Session::new();
|
|
s.ui_requests.clear();
|
|
s.execute("proto.presentOptions", &json!({"scale": "fitWidth", "deviceFrame": false})).unwrap();
|
|
let r = s.ui_requests.last().unwrap();
|
|
assert_eq!((r.kind.as_str(), &r.data), ("present.options", &json!({"scale": "fitWidth", "deviceFrame": false})));
|
|
for scale in crate::cmd::proto::PRESENT_SCALES {
|
|
s.execute("proto.presentOptions", &json!({"scale": scale})).unwrap();
|
|
}
|
|
assert!(s.execute("proto.presentOptions", &json!({"scale": "huge"})).is_err());
|
|
assert!(s.execute("proto.presentOptions", &json!({"deviceFrame": "yes"})).is_err());
|
|
assert!(s.execute("proto.presentOptions", &json!({})).is_err(), "nothing to set");
|
|
// It's a viewing option: the document doesn't change.
|
|
assert!(!s.ds().unwrap().is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_variable_modes_go_back_to_auto() {
|
|
let mut s = Session::new();
|
|
let id = |v: serde_json::Value| id_of(&v["id"]).unwrap();
|
|
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap());
|
|
let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10, "parent": f})).unwrap());
|
|
s.execute("variable.createCollection", &json!({"name": "Tokens", "modes": ["Light", "Dark"]})).unwrap();
|
|
s.execute("variable.create", &json!({"collection": "Tokens", "name": "size", "type": "number", "value": 20})).unwrap();
|
|
s.execute("variable.set", &json!({"variable": "size", "mode": "Dark", "value": 40})).unwrap();
|
|
s.execute("variable.bind", &json!({"ids": [r], "field": "width", "variable": "size"})).unwrap();
|
|
let width = |s: &Session| s.doc().unwrap().node(r).unwrap().width;
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Dark"})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
// Auto: the frame inherits the page's mode again, and the layer follows.
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": null})).unwrap();
|
|
assert!(s.doc().unwrap().node(f).unwrap().var_modes.is_empty());
|
|
assert_eq!(width(&s), 20.0);
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
// The page set to Dark: Auto inherits it.
|
|
let page = s.page().unwrap();
|
|
s.execute("variable.setMode", &json!({"ids": [page], "collection": "Tokens", "mode": "Dark"})).unwrap();
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Light"})).unwrap();
|
|
assert_eq!(width(&s), 20.0);
|
|
s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": null})).unwrap();
|
|
assert_eq!(width(&s), 40.0);
|
|
assert!(s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens", "mode": "Dusk"})).is_err());
|
|
assert!(s.execute("variable.setMode", &json!({"ids": [f], "collection": "Tokens"})).is_err(), "a mode, or null for Auto");
|
|
}
|