mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Keep View toggles such as Rulers in the preferences
Fixes #379 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
833f0d2c27
commit
3d90d8edfe
@@ -19,6 +19,48 @@ fn flag(s: &mut Session, p: &Value, f: fn(&mut ViewState) -> &mut bool) -> Resul
|
||||
Ok(json!({"on": *slot}))
|
||||
}
|
||||
|
||||
/// One of a view's on/off settings.
|
||||
type Slot = fn(&mut ViewState) -> &mut bool;
|
||||
|
||||
/// The View menu's toggles the preferences keep: files opened or made later start with them.
|
||||
const KEPT: &[(&str, Slot)] = &[
|
||||
("view.pixelGrid", |v| &mut v.pixel_grid),
|
||||
("view.layoutGrids", |v| &mut v.layout_grids),
|
||||
("view.rulers", |v| &mut v.rulers),
|
||||
("view.slices", |v| &mut v.show_slices),
|
||||
("view.comments", |v| &mut v.comments),
|
||||
("view.multiplayerCursors", |v| &mut v.multiplayer_cursors),
|
||||
("view.outlines", |v| &mut v.outlines),
|
||||
("view.outlinesSelection", |v| &mut v.outlines_selection),
|
||||
("view.outlinesHiddenLayers", |v| &mut v.outline_hidden),
|
||||
("view.outlinesObjectBounds", |v| &mut v.outline_bounds),
|
||||
("view.maskOutlines", |v| &mut v.mask_outlines),
|
||||
("view.frameOutlines", |v| &mut v.frame_outlines),
|
||||
("view.proofSrgb", |v| &mut v.proof_srgb),
|
||||
("view.snapGeometry", |v| &mut v.snap_geometry),
|
||||
("view.snapObjects", |v| &mut v.snap_objects),
|
||||
("view.snapPixel", |v| &mut v.snap_pixel),
|
||||
];
|
||||
|
||||
/// A View toggle the preferences keep: the file's view changes and the preferences remember it.
|
||||
fn kept(s: &mut Session, p: &Value, id: &str) -> Result<Value> {
|
||||
let Some((_, f)) = KEPT.iter().find(|(k, _)| *k == id) else { return Err(bad(id, "not a View toggle")) };
|
||||
let r = flag(s, p, *f)?;
|
||||
if let Some(on) = r.get("on").and_then(Value::as_bool) {
|
||||
s.prefs.view.insert(id.to_string(), on);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Set `v`'s View toggles to the ones the preferences keep (the others stay as they are).
|
||||
pub(crate) fn apply_kept(prefs: &crate::Prefs, v: &mut ViewState) {
|
||||
for (id, f) in KEPT {
|
||||
if let Some(on) = prefs.view.get(*id) {
|
||||
*f(v) = *on;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the View menu's toggle `id` is on (the menus show a check mark); `None` for a command
|
||||
/// that isn't one.
|
||||
pub fn checked(s: &Session, id: &str) -> Option<bool> {
|
||||
@@ -227,46 +269,46 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!("view.nextFrame", "Zoom to next frame", "{}", false, |s, _| zoom_frame(s, 1)),
|
||||
spec!("view.prevPage", "Previous page", "{}", false, |s, _| page_step(s, -1)),
|
||||
spec!("view.nextPage", "Next page", "{}", false, |s, _| page_step(s, 1)),
|
||||
spec!("view.pixelGrid", "Pixel grid", "{on?}", false, |s, p| flag(s, p, |v| &mut v.pixel_grid)),
|
||||
spec!("view.layoutGrids", "Layout grids", "{on?}", false, |s, p| flag(s, p, |v| &mut v.layout_grids)),
|
||||
spec!("view.rulers", "Rulers", "{on?}", false, |s, p| flag(s, p, |v| &mut v.rulers)),
|
||||
spec!("view.slices", "Slices", "{on?}", false, |s, p| flag(s, p, |v| &mut v.show_slices)),
|
||||
spec!("view.comments", "Comments", "{on?}", false, |s, p| flag(s, p, |v| &mut v.comments)),
|
||||
spec!("view.pixelGrid", "Pixel grid", "{on?}", false, |s, p| kept(s, p, "view.pixelGrid")),
|
||||
spec!("view.layoutGrids", "Layout grids", "{on?}", false, |s, p| kept(s, p, "view.layoutGrids")),
|
||||
spec!("view.rulers", "Rulers", "{on?}", false, |s, p| kept(s, p, "view.rulers")),
|
||||
spec!("view.slices", "Slices", "{on?}", false, |s, p| kept(s, p, "view.slices")),
|
||||
spec!("view.comments", "Comments", "{on?}", false, |s, p| kept(s, p, "view.comments")),
|
||||
spec!("view.multiplayerCursors", "Multiplayer cursors", "{on?} others' cursors in a live session", false, |s, p| {
|
||||
flag(s, p, |v| &mut v.multiplayer_cursors)
|
||||
kept(s, p, "view.multiplayerCursors")
|
||||
}),
|
||||
spec!("view.pan", "Pan the view", "{dx, dy (arrow-key steps, e.g. 1 or -1), big? (ten times as far, like Shift)}", false, pan),
|
||||
spec!("view.outlines", "Outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.outlines)),
|
||||
spec!("view.outlines", "Outlines", "{on?}", false, |s, p| kept(s, p, "view.outlines")),
|
||||
spec!("view.outlinesSelection", "Outlines for selection", "{on?} outline mode for the selected layers only", false, |s, p| {
|
||||
flag(s, p, |v| &mut v.outlines_selection)
|
||||
kept(s, p, "view.outlinesSelection")
|
||||
}),
|
||||
spec!(
|
||||
"view.outlinesHiddenLayers",
|
||||
"Include hidden layers in outlines",
|
||||
"{on?} outline mode (and outlines for the selection) outlines hidden layers too; they can be clicked there",
|
||||
false,
|
||||
|s, p| { flag(s, p, |v| &mut v.outline_hidden) }
|
||||
|s, p| { kept(s, p, "view.outlinesHiddenLayers") }
|
||||
),
|
||||
spec!(
|
||||
"view.outlinesObjectBounds",
|
||||
"Include object bounds in outlines",
|
||||
"{on?} outline mode also draws each layer's box (groups, text, vectors); a click on a box's edge picks its layer",
|
||||
false,
|
||||
|s, p| { flag(s, p, |v| &mut v.outline_bounds) }
|
||||
|s, p| { kept(s, p, "view.outlinesObjectBounds") }
|
||||
),
|
||||
spec!("view.maskOutlines", "Mask outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.mask_outlines)),
|
||||
spec!("view.maskOutlines", "Mask outlines", "{on?}", false, |s, p| kept(s, p, "view.maskOutlines")),
|
||||
spec!("view.pixelPreview", "Pixel preview", "{on?, scale? (1|2: rendered pixels per page pixel; turns it on)}", false, pixel_preview),
|
||||
spec!("view.frameOutlines", "Frame outlines", "{on?}", false, |s, p| flag(s, p, |v| &mut v.frame_outlines)),
|
||||
spec!("view.frameOutlines", "Frame outlines", "{on?}", false, |s, p| kept(s, p, "view.frameOutlines")),
|
||||
spec!(
|
||||
"view.proofSrgb",
|
||||
"Soft-proof sRGB",
|
||||
"{on?} mark the colours of a Display P3 file that sRGB screens and files can't show",
|
||||
false,
|
||||
|s, p| { flag(s, p, |v| &mut v.proof_srgb) }
|
||||
|s, p| { kept(s, p, "view.proofSrgb") }
|
||||
),
|
||||
spec!("view.snapGeometry", "Snap to geometry", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_geometry)),
|
||||
spec!("view.snapObjects", "Snap to objects", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_objects)),
|
||||
spec!("view.snapPixel", "Snap to pixel grid", "{on?}", false, |s, p| flag(s, p, |v| &mut v.snap_pixel)),
|
||||
spec!("view.snapGeometry", "Snap to geometry", "{on?}", false, |s, p| kept(s, p, "view.snapGeometry")),
|
||||
spec!("view.snapObjects", "Snap to objects", "{on?}", false, |s, p| kept(s, p, "view.snapObjects")),
|
||||
spec!("view.snapPixel", "Snap to pixel grid", "{on?}", false, |s, p| kept(s, p, "view.snapPixel")),
|
||||
spec!("view.toggleUi", "Show/Hide UI", "{on?}", false, |s, p| flag(s, p, |v| &mut v.ui_hidden)),
|
||||
spec!("view.fullscreen", "Full screen", "{on?} the window fills the screen (the desktop app; a no-op headless)", false, |s, p| {
|
||||
s.request_ui("window.fullscreen", json!({"on": boolean(p, "on")}));
|
||||
|
||||
@@ -328,6 +328,8 @@ pub struct DocState {
|
||||
pub guide: Option<(NodeId, usize)>,
|
||||
/// The selection's rotation origin, when moved or shown (see [`rotation_origin`]; not saved).
|
||||
pub(crate) rotation_origin: Option<rotation_origin::RotationOrigin>,
|
||||
/// The View toggles the preferences keep were applied ([`Session::apply_view_prefs`]).
|
||||
view_from_prefs: bool,
|
||||
}
|
||||
|
||||
/// A version shown on the canvas in place of the document: read-only until it's restored or the
|
||||
@@ -383,6 +385,7 @@ impl DocState {
|
||||
focus: None,
|
||||
guide: None,
|
||||
rotation_origin: None,
|
||||
view_from_prefs: false,
|
||||
};
|
||||
st.zoom_to_fit();
|
||||
st
|
||||
@@ -599,6 +602,10 @@ pub struct Prefs {
|
||||
pub home_sort: String,
|
||||
/// Which way: `"desc"` (newest first, Z to A) or `"asc"`.
|
||||
pub home_order: String,
|
||||
/// The View menu's toggles turned on or off (rulers, pixel grid, snapping…), by command id:
|
||||
/// files opened or made later start with them. Missing: the toggle's default.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub view: BTreeMap<String, bool>,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
@@ -627,6 +634,7 @@ impl Default for Prefs {
|
||||
home_views: BTreeMap::new(),
|
||||
home_sort: "modified".into(),
|
||||
home_order: "desc".into(),
|
||||
view: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -764,6 +772,14 @@ impl Session {
|
||||
s
|
||||
}
|
||||
|
||||
/// Files new to the session start with the View toggles the preferences keep.
|
||||
pub fn apply_view_prefs(&mut self) {
|
||||
for ds in self.docs.iter_mut().filter(|d| !d.view_from_prefs) {
|
||||
cmd::view::apply_kept(&self.prefs, &mut ds.view);
|
||||
ds.view_from_prefs = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ds(&self) -> Result<&DocState> {
|
||||
self.docs.get(self.active).ok_or(EngineError::NoDocument)
|
||||
}
|
||||
@@ -805,6 +821,7 @@ impl Session {
|
||||
return Err(EngineError::Disabled(id.into(), "leave the live session before reverting to the saved file".into()));
|
||||
}
|
||||
branches::sync(self);
|
||||
self.apply_view_prefs();
|
||||
if textpreview::COMMANDS.contains(&id) {
|
||||
// A live preview is no edit: no undo step, normalisation or rollback here.
|
||||
let r = guard::run(id, || (spec.run)(self, params));
|
||||
@@ -828,6 +845,7 @@ impl Session {
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
self.after_command(spec, before);
|
||||
self.apply_view_prefs();
|
||||
if spec.history && id != "edit.repeat" {
|
||||
self.last_command = Some((id.to_string(), params.clone()));
|
||||
}
|
||||
|
||||
@@ -725,3 +725,27 @@ fn alt_measures_between_layers() {
|
||||
assert_eq!(measured(&mut s, p, m), want, "{why}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_toggles_are_kept_for_the_next_launch() {
|
||||
let mut s = Session::new();
|
||||
// (command, turned to, ViewState field, derivation)
|
||||
let rows: Vec<(&str, bool, fn(&ViewState) -> bool, &str)> = vec![
|
||||
("view.rulers", true, |v| v.rulers, "rulers shown stay shown"),
|
||||
("view.pixelGrid", false, |v| v.pixel_grid, "a hidden pixel grid stays hidden"),
|
||||
("view.layoutGrids", false, |v| v.layout_grids, "hidden layout grids stay hidden"),
|
||||
("view.snapPixel", false, |v| v.snap_pixel, "snapping to the pixel grid off stays off"),
|
||||
];
|
||||
for (cmd, on, _, _) in &rows {
|
||||
ex(&mut s, cmd, json!({"on": on}));
|
||||
}
|
||||
// The app writes its preferences as JSON and reads them back at the next launch.
|
||||
let saved = serde_json::to_value(&s.prefs).unwrap();
|
||||
let mut next = Session::empty();
|
||||
next.prefs = serde_json::from_value(saved).unwrap();
|
||||
ex(&mut next, "file.new", json!({}));
|
||||
let view = &next.ds().unwrap().view;
|
||||
for (cmd, on, get, why) in rows {
|
||||
assert_eq!(get(view), on, "{cmd}: {why}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -942,6 +942,8 @@ impl FigApp {
|
||||
}
|
||||
self.background_done(&ctx);
|
||||
}
|
||||
// Files that arrived without a command (joined, recovered) take the kept View toggles too.
|
||||
self.session.apply_view_prefs();
|
||||
windows::begin(self, &ctx);
|
||||
let (files, prefs) = (self.session.docs.len(), self.session.prefs.clone());
|
||||
self.frame(ui);
|
||||
|
||||
Reference in New Issue
Block a user