diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index 61c592e0..69b938e9 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -22,31 +22,33 @@ fn flag(s: &mut Session, p: &Value, f: fn(&mut ViewState) -> &mut bool) -> Resul /// 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), +/// The View menu's on/off toggles (Pixel preview, with its scale, has its own command), and +/// whether the preferences keep each: files opened or made later start with the kept ones. Outline +/// modes and the sRGB soft-proof stay with their file, so a new file never opens in one. +const TOGGLES: &[(&str, Slot, bool)] = &[ + ("view.pixelGrid", |v| &mut v.pixel_grid, true), + ("view.layoutGrids", |v| &mut v.layout_grids, true), + ("view.rulers", |v| &mut v.rulers, true), + ("view.slices", |v| &mut v.show_slices, true), + ("view.comments", |v| &mut v.comments, true), + ("view.multiplayerCursors", |v| &mut v.multiplayer_cursors, true), + ("view.outlines", |v| &mut v.outlines, false), + ("view.outlinesSelection", |v| &mut v.outlines_selection, false), + ("view.outlinesHiddenLayers", |v| &mut v.outline_hidden, true), + ("view.outlinesObjectBounds", |v| &mut v.outline_bounds, true), + ("view.maskOutlines", |v| &mut v.mask_outlines, true), + ("view.frameOutlines", |v| &mut v.frame_outlines, true), + ("view.proofSrgb", |v| &mut v.proof_srgb, false), + ("view.snapGeometry", |v| &mut v.snap_geometry, true), + ("view.snapObjects", |v| &mut v.snap_objects, true), + ("view.snapPixel", |v| &mut v.snap_pixel, true), ]; -/// 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 { - 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) { +/// View toggle `id`: the file's view changes, and the preferences remember a kept one. +fn toggle(s: &mut Session, p: &Value, id: &str) -> Result { + let Some(&(_, f, keep)) = TOGGLES.iter().find(|(k, ..)| *k == id) else { return Err(bad(id, "not a View toggle")) }; + let r = flag(s, p, f)?; + if keep && let Some(on) = r.get("on").and_then(Value::as_bool) { s.prefs.view.insert(id.to_string(), on); } Ok(r) @@ -54,7 +56,7 @@ fn kept(s: &mut Session, p: &Value, id: &str) -> Result { /// 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 { + for (id, f, _) in TOGGLES.iter().filter(|t| t.2) { if let Some(on) = prefs.view.get(*id) { *f(v) = *on; } @@ -64,27 +66,11 @@ pub(crate) fn apply_kept(prefs: &crate::Prefs, v: &mut ViewState) { /// 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 { - let v = &s.ds().ok()?.view; - Some(match id { - "view.pixelGrid" => v.pixel_grid, - "view.layoutGrids" => v.layout_grids, - "view.rulers" => v.rulers, - "view.slices" => v.show_slices, - "view.comments" => v.comments, - "view.multiplayerCursors" => v.multiplayer_cursors, - "view.outlines" => v.outlines, - "view.outlinesSelection" => v.outlines_selection, - "view.outlinesHiddenLayers" => v.outline_hidden, - "view.outlinesObjectBounds" => v.outline_bounds, - "view.maskOutlines" => v.mask_outlines, - "view.pixelPreview" => v.pixel_preview, - "view.frameOutlines" => v.frame_outlines, - "view.proofSrgb" => v.proof_srgb, - "view.snapGeometry" => v.snap_geometry, - "view.snapObjects" => v.snap_objects, - "view.snapPixel" => v.snap_pixel, - _ => return None, - }) + let mut v = s.ds().ok()?.view.clone(); + if id == "view.pixelPreview" { + return Some(v.pixel_preview); + } + TOGGLES.iter().find(|(k, ..)| *k == id).map(|(_, f, _)| *f(&mut v)) } /// Pixel preview: toggles, or turns on at a resolution of 1x or 2x. @@ -277,46 +263,46 @@ pub fn specs() -> Vec { 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| 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.pixelGrid", "Pixel grid", "{on?}", false, |s, p| toggle(s, p, "view.pixelGrid")), + spec!("view.layoutGrids", "Layout grids", "{on?}", false, |s, p| toggle(s, p, "view.layoutGrids")), + spec!("view.rulers", "Rulers", "{on?}", false, |s, p| toggle(s, p, "view.rulers")), + spec!("view.slices", "Slices", "{on?}", false, |s, p| toggle(s, p, "view.slices")), + spec!("view.comments", "Comments", "{on?}", false, |s, p| toggle(s, p, "view.comments")), spec!("view.multiplayerCursors", "Multiplayer cursors", "{on?} others' cursors in a live session", false, |s, p| { - kept(s, p, "view.multiplayerCursors") + toggle(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| kept(s, p, "view.outlines")), + spec!("view.outlines", "Outlines", "{on?}", false, |s, p| toggle(s, p, "view.outlines")), spec!("view.outlinesSelection", "Outlines for selection", "{on?} outline mode for the selected layers only", false, |s, p| { - kept(s, p, "view.outlinesSelection") + toggle(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| { kept(s, p, "view.outlinesHiddenLayers") } + |s, p| { toggle(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| { kept(s, p, "view.outlinesObjectBounds") } + |s, p| { toggle(s, p, "view.outlinesObjectBounds") } ), - spec!("view.maskOutlines", "Mask outlines", "{on?}", false, |s, p| kept(s, p, "view.maskOutlines")), + spec!("view.maskOutlines", "Mask outlines", "{on?}", false, |s, p| toggle(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| kept(s, p, "view.frameOutlines")), + spec!("view.frameOutlines", "Frame outlines", "{on?}", false, |s, p| toggle(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| { kept(s, p, "view.proofSrgb") } + |s, p| { toggle(s, p, "view.proofSrgb") } ), - 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.snapGeometry", "Snap to geometry", "{on?}", false, |s, p| toggle(s, p, "view.snapGeometry")), + spec!("view.snapObjects", "Snap to objects", "{on?}", false, |s, p| toggle(s, p, "view.snapObjects")), + spec!("view.snapPixel", "Snap to pixel grid", "{on?}", false, |s, p| toggle(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")})); diff --git a/crates/engine/src/view_behaviour_tests.rs b/crates/engine/src/view_behaviour_tests.rs index f8d9750c..7f9e5a84 100644 --- a/crates/engine/src/view_behaviour_tests.rs +++ b/crates/engine/src/view_behaviour_tests.rs @@ -756,6 +756,10 @@ fn view_toggles_are_kept_for_the_next_launch() { for (cmd, on, _, _) in &rows { ex(&mut s, cmd, json!({"on": on})); } + // Modes stay with their file: a new one never opens in outline mode or soft-proofing. + for cmd in ["view.outlines", "view.outlinesSelection", "view.proofSrgb"] { + ex(&mut s, cmd, json!({"on": true})); + } // 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(); @@ -765,4 +769,5 @@ fn view_toggles_are_kept_for_the_next_launch() { for (cmd, on, get, why) in rows { assert_eq!(get(view), on, "{cmd}: {why}"); } + assert!(!view.outlines && !view.outlines_selection && !view.proof_srgb, "{view:?}"); }