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:
NetterSilver
2026-10-15 03:33:28 +02:00
co-authored by Claude Haiku 5.5
parent 833f0d2c27
commit 3d90d8edfe
4 changed files with 102 additions and 16 deletions
+58 -16
View File
@@ -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")}));
+18
View File
@@ -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()));
}
+24
View File
@@ -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}");
}
}
+2
View File
@@ -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);