mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
continued work; wip
This commit is contained in:
@@ -17,6 +17,7 @@ mod paint;
|
||||
mod panelcmds;
|
||||
mod path;
|
||||
mod pathops;
|
||||
pub mod prefscmds;
|
||||
mod recolor;
|
||||
mod select;
|
||||
mod textedit;
|
||||
@@ -143,6 +144,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(panelcmds::specs());
|
||||
v.extend(buildcmds::specs());
|
||||
v.extend(brushsym::specs());
|
||||
v.extend(prefscmds::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
//! Preferences (Edit → Preferences): `prefs.get`, `prefs.set`, `prefs.reset`, `prefs.list`.
|
||||
//!
|
||||
//! [`PREF_SPECS`] is the single source of truth for every preference: its category, section,
|
||||
//! label and value kind. The Preferences dialog renders from it and `prefs.set` validates with it.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::*;
|
||||
use crate::Prefs;
|
||||
|
||||
/// What kind of value a preference holds.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PrefKind {
|
||||
Bool,
|
||||
/// A number in `min..=max` shown with `unit` ("pt", "px", "°", "%", "min", "").
|
||||
Num { min: f64, max: f64, unit: &'static str },
|
||||
/// A whole number in `min..=max`.
|
||||
Int { min: i64, max: i64 },
|
||||
/// One of (value, label).
|
||||
Choice(&'static [(&'static str, &'static str)]),
|
||||
/// `#rrggbb`.
|
||||
Color,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PrefSpec {
|
||||
pub key: &'static str,
|
||||
pub category: &'static str,
|
||||
/// Group heading inside the category ("" = none).
|
||||
pub section: &'static str,
|
||||
pub label: &'static str,
|
||||
pub kind: PrefKind,
|
||||
}
|
||||
|
||||
/// Categories in Illustrator's order (Plug-ins & Scratch Disks is "Performance & Storage" here).
|
||||
pub const PREF_CATEGORIES: &[&str] = &[
|
||||
"General",
|
||||
"Selection & Anchor Display",
|
||||
"Type",
|
||||
"Units",
|
||||
"Guides & Grid",
|
||||
"Smart Guides",
|
||||
"Slices",
|
||||
"Hyphenation",
|
||||
"Performance & Storage",
|
||||
"User Interface",
|
||||
"Performance",
|
||||
"File Handling",
|
||||
"Clipboard Handling",
|
||||
"Appearance of Black",
|
||||
"Devices",
|
||||
];
|
||||
|
||||
pub const UNITS: &[(&str, &str)] = &[
|
||||
("points", "Points"),
|
||||
("picas", "Picas"),
|
||||
("inches", "Inches"),
|
||||
("millimeters", "Millimeters"),
|
||||
("centimeters", "Centimeters"),
|
||||
("pixels", "Pixels"),
|
||||
];
|
||||
const LINE_STYLE: &[(&str, &str)] = &[("lines", "Lines"), ("dots", "Dots")];
|
||||
const BLACK: &[(&str, &str)] = &[("accurate", "Display All Blacks Accurately"), ("rich", "Display All Blacks as Rich Black")];
|
||||
const BLACK_OUT: &[(&str, &str)] = &[("accurate", "Output All Blacks Accurately"), ("rich", "Output All Blacks as Rich Black")];
|
||||
|
||||
macro_rules! p {
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, bool) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Bool }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, num($min:expr, $max:expr, $unit:literal)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Num { min: $min, max: $max, unit: $unit } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, int($min:expr, $max:expr)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Int { min: $min, max: $max } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, choice($c:expr)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Choice($c) }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, color) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Color }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, text) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Text }
|
||||
};
|
||||
}
|
||||
|
||||
pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
// General
|
||||
p!("keyboardIncrement", "General", "", "Keyboard Increment", num(0.001, 1296.0, "pt")),
|
||||
p!("constrainAngle", "General", "", "Constrain Angle", num(-360.0, 360.0, "°")),
|
||||
p!("cornerRadius", "General", "", "Corner Radius", num(0.0, 1296.0, "pt")),
|
||||
p!("pasteOffset", "General", "", "Paste Offset", num(0.0, 1296.0, "pt")),
|
||||
p!("disableAutoAddDelete", "General", "Options", "Disable Auto Add/Delete", bool),
|
||||
p!("usePreciseCursors", "General", "Options", "Use Precise Cursors", bool),
|
||||
p!("showToolTips", "General", "Options", "Show Tool Tips", bool),
|
||||
p!("antiAliasedArtwork", "General", "Options", "Anti-aliased Artwork", bool),
|
||||
p!("selectSameTintPercent", "General", "Options", "Select Same Tint %", bool),
|
||||
p!("showHomeScreen", "General", "Options", "Show The Home Screen When No Documents Are Open", bool),
|
||||
p!("usePreviewBounds", "General", "Options", "Use Preview Bounds", bool),
|
||||
p!("displayPrintSize", "General", "Options", "Display Print Size at 100% Zoom", bool),
|
||||
p!("doubleClickToIsolate", "General", "Options", "Double Click To Isolate", bool),
|
||||
p!("transformPatternTiles", "General", "Options", "Transform Pattern Tiles", bool),
|
||||
p!("scaleCorners", "General", "Options", "Scale Corners", bool),
|
||||
p!("scaleStrokes", "General", "Options", "Scale Strokes & Effects", bool),
|
||||
p!("zoomWithMouseWheel", "General", "Options", "Zoom with Mouse Wheel", bool),
|
||||
// Selection & Anchor Display
|
||||
p!("selectionTolerance", "Selection & Anchor Display", "Selection", "Tolerance", num(1.0, 8.0, "px")),
|
||||
p!("objectSelectionByPathOnly", "Selection & Anchor Display", "Selection", "Object Selection by Path Only", bool),
|
||||
p!("snapToPointTolerance", "Selection & Anchor Display", "Selection", "Snap to Point", num(1.0, 8.0, "px")),
|
||||
p!("ctrlClickSelectsBehind", "Selection & Anchor Display", "Selection", "Command Click to Select Objects Behind", bool),
|
||||
p!("zoomToSelection", "Selection & Anchor Display", "Selection", "Zoom to Selection", bool),
|
||||
p!("moveLockedWithArtboard", "Selection & Anchor Display", "Selection", "Move Locked and Hidden Artwork with Artboard", bool),
|
||||
p!("anchorSize", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Size", int(1, 7)),
|
||||
p!(
|
||||
"handleStyle",
|
||||
"Selection & Anchor Display",
|
||||
"Anchor Points, Handle, and Bounding Box Display",
|
||||
"Handles",
|
||||
choice(&[("solid", "Solid"), ("hollow", "Hollow"), ("large", "Large")])
|
||||
),
|
||||
p!("highlightAnchorsOnHover", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Highlight anchors on mouse over", bool),
|
||||
p!(
|
||||
"showHandlesMultipleAnchors",
|
||||
"Selection & Anchor Display",
|
||||
"Anchor Points, Handle, and Bounding Box Display",
|
||||
"Show handles when multiple anchors are selected",
|
||||
bool
|
||||
),
|
||||
p!("hideCornerWidgetAbove", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Hide Corner Widget for angles greater than", num(0.0, 180.0, "°")),
|
||||
p!("penRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Pen Tool", bool),
|
||||
p!("curvatureRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Curvature Tool", bool),
|
||||
// Type
|
||||
p!("typeSizeIncrement", "Type", "", "Size/Leading", num(0.001, 1296.0, "pt")),
|
||||
p!("trackingIncrement", "Type", "", "Tracking", num(1.0, 1000.0, "/1000 em")),
|
||||
p!("baselineShiftIncrement", "Type", "", "Baseline Shift", num(0.001, 1296.0, "pt")),
|
||||
p!("showEastAsianOptions", "Type", "Language Options", "Show East Asian Options", bool),
|
||||
p!("showIndicOptions", "Type", "Language Options", "Show Indic Options", bool),
|
||||
p!("typeSelectionByPathOnly", "Type", "Options", "Type Object Selection by Path Only", bool),
|
||||
p!("fontNamesInEnglish", "Type", "Options", "Show Font Names in English", bool),
|
||||
p!("autoSizeAreaType", "Type", "Options", "Auto Size New Area Type", bool),
|
||||
p!("fontPreview", "Type", "Options", "Enable in-menu font previews", bool),
|
||||
p!("fontPreviewSize", "Type", "Options", "Font Preview Size", choice(&[("small", "Small"), ("medium", "Medium"), ("large", "Large")])),
|
||||
p!("recentFontsCount", "Type", "Options", "Number of Recent Fonts", int(1, 15)),
|
||||
p!("missingGlyphProtection", "Type", "Options", "Enable Missing Glyph Protection", bool),
|
||||
p!("highlightAlternateGlyphs", "Type", "Options", "Highlight Alternate Glyphs", bool),
|
||||
p!("placeholderText", "Type", "Options", "Fill New Type Objects With Placeholder Text", bool),
|
||||
// Units
|
||||
p!("unitsGeneral", "Units", "", "General", choice(UNITS)),
|
||||
p!("unitsStroke", "Units", "", "Stroke", choice(UNITS)),
|
||||
p!("unitsType", "Units", "", "Type", choice(UNITS)),
|
||||
p!("unitsAsianType", "Units", "", "East Asian Type", choice(UNITS)),
|
||||
p!("numbersWithoutUnitsArePoints", "Units", "", "Numbers Without Units Are Points", bool),
|
||||
p!("identifyObjectsBy", "Units", "", "Identify Objects By", choice(&[("objectName", "Object Name"), ("xmlId", "XML ID")])),
|
||||
// Guides & Grid
|
||||
p!("guideColor", "Guides & Grid", "Guides", "Color", color),
|
||||
p!("guideStyle", "Guides & Grid", "Guides", "Style", choice(LINE_STYLE)),
|
||||
p!("gridColor", "Guides & Grid", "Grid", "Color", color),
|
||||
p!("gridStyle", "Guides & Grid", "Grid", "Style", choice(LINE_STYLE)),
|
||||
p!("gridlineEvery", "Guides & Grid", "Grid", "Gridline every", num(1.0, 16383.0, "pt")),
|
||||
p!("gridSubdivisions", "Guides & Grid", "Grid", "Subdivisions", int(1, 1000)),
|
||||
p!("gridsInBack", "Guides & Grid", "Grid", "Grids In Back", bool),
|
||||
p!("showPixelGrid", "Guides & Grid", "Grid", "Show Pixel Grid (Above 600% Zoom)", bool),
|
||||
// Smart Guides
|
||||
p!("smartGuideColor", "Smart Guides", "Display Options", "Color", color),
|
||||
p!("alignmentGuides", "Smart Guides", "Display Options", "Alignment Guides", bool),
|
||||
p!("objectHighlighting", "Smart Guides", "Display Options", "Object Highlighting", bool),
|
||||
p!("transformToolsGuides", "Smart Guides", "Display Options", "Transform Tools", bool),
|
||||
p!("constructionGuides", "Smart Guides", "Display Options", "Construction Guides", bool),
|
||||
p!(
|
||||
"constructionAngles",
|
||||
"Smart Guides",
|
||||
"Display Options",
|
||||
"Angles",
|
||||
choice(&[
|
||||
("90° & 45° Angles", "90° & 45° Angles"),
|
||||
("30° Angles", "30° Angles"),
|
||||
("45° Angles", "45° Angles"),
|
||||
("60° Angles", "60° Angles"),
|
||||
("90° Angles", "90° Angles"),
|
||||
("90° & 45° & 30° Angles", "90° & 45° & 30° Angles"),
|
||||
])
|
||||
),
|
||||
p!("anchorPathLabels", "Smart Guides", "Display Options", "Anchor/Path Labels", bool),
|
||||
p!("measurementLabels", "Smart Guides", "Display Options", "Measurement Labels", bool),
|
||||
p!("spacingGuides", "Smart Guides", "Display Options", "Spacing Guides", bool),
|
||||
p!("snappingTolerance", "Smart Guides", "", "Snapping Tolerance", num(0.0, 40.0, "pt")),
|
||||
// Slices
|
||||
p!("showSliceNumbers", "Slices", "", "Show Slice Numbers", bool),
|
||||
p!("sliceLineColor", "Slices", "", "Line Color", color),
|
||||
// Hyphenation
|
||||
p!(
|
||||
"hyphenationLanguage",
|
||||
"Hyphenation",
|
||||
"",
|
||||
"Default Language",
|
||||
choice(&[
|
||||
("English: USA", "English: USA"),
|
||||
("English: UK", "English: UK"),
|
||||
("French", "French"),
|
||||
("German", "German"),
|
||||
("Spanish", "Spanish"),
|
||||
("Italian", "Italian"),
|
||||
("Dutch", "Dutch"),
|
||||
("Portuguese", "Portuguese")
|
||||
])
|
||||
),
|
||||
p!("hyphenationExceptions", "Hyphenation", "", "Exceptions (comma separated)", text),
|
||||
// Performance & Storage
|
||||
p!("pluginsFolder", "Performance & Storage", "Plug-ins", "Additional Plug-ins Folder", text),
|
||||
p!("scratchPrimary", "Performance & Storage", "Scratch Disks", "Primary", text),
|
||||
p!("scratchSecondary", "Performance & Storage", "Scratch Disks", "Secondary", text),
|
||||
// User Interface
|
||||
p!(
|
||||
"uiBrightness",
|
||||
"User Interface",
|
||||
"",
|
||||
"Brightness",
|
||||
choice(&[("dark", "Dark"), ("mediumDark", "Medium Dark"), ("mediumLight", "Medium Light"), ("light", "Light")])
|
||||
),
|
||||
p!("canvasColor", "User Interface", "", "Canvas Color", choice(&[("matchUi", "Match User Interface Brightness"), ("white", "White")])),
|
||||
p!("autoCollapseIconPanels", "User Interface", "", "Auto-Collapse Iconic Panels", bool),
|
||||
p!("openDocumentsAsTabs", "User Interface", "", "Open Documents As Tabs", bool),
|
||||
p!("largeTabs", "User Interface", "", "Large Tabs", bool),
|
||||
p!("uiScaling", "User Interface", "UI Scaling", "Scale", num(0.75, 2.0, "×")),
|
||||
p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool),
|
||||
// Performance
|
||||
p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool),
|
||||
p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool),
|
||||
p!("historyStates", "Performance", "", "History States", int(5, 1000)),
|
||||
p!("realTimeDrawing", "Performance", "", "Real-time Drawing and Editing", bool),
|
||||
p!("renderThreads", "Performance", "", "Render Threads (-1 = Automatic)", int(-1, 64)),
|
||||
// File Handling
|
||||
p!("backgroundSave", "File Handling", "", "Enable Background Save", bool),
|
||||
p!("backgroundExport", "File Handling", "", "Enable Background Export", bool),
|
||||
p!("autosaveRecovery", "File Handling", "Data Recovery", "Automatically Save Recovery Data", bool),
|
||||
p!("autosaveInterval", "File Handling", "Data Recovery", "Every (minutes)", int(1, 60)),
|
||||
p!("recoveryFolder", "File Handling", "Data Recovery", "Folder", text),
|
||||
p!("recoveryOffForComplex", "File Handling", "Data Recovery", "Turn off Data Recovery for complex documents", bool),
|
||||
p!("recentFilesCount", "File Handling", "Files", "Number of Recent Files to Display", int(0, 30)),
|
||||
p!("lowResProxyEps", "File Handling", "Files", "Use Low Resolution Proxy for Linked EPS", bool),
|
||||
p!("antiAliasedBitmaps", "File Handling", "Files", "Display Bitmaps as Anti-aliased Images in Pixel Preview", bool),
|
||||
p!(
|
||||
"updateLinks",
|
||||
"File Handling",
|
||||
"Files",
|
||||
"Update Links",
|
||||
choice(&[("automatically", "Automatically"), ("manually", "Manually"), ("askWhenModified", "Ask When Modified")])
|
||||
),
|
||||
// Clipboard Handling
|
||||
p!("copyAsSvg", "Clipboard Handling", "On Copy", "Include SVG Code", bool),
|
||||
p!("copyAsPdf", "Clipboard Handling", "On Copy", "PDF", bool),
|
||||
p!("copyAicb", "Clipboard Handling", "On Copy", "AICB (no transparency support)", bool),
|
||||
p!("aicbMode", "Clipboard Handling", "On Copy", "AICB", choice(&[("preservePaths", "Preserve Paths"), ("preserveAppearance", "Preserve Appearance and Overprints")])),
|
||||
p!("pasteTextFormatting", "Clipboard Handling", "On Paste", "When pasting text", choice(&[("keep", "Keep Formatting"), ("plain", "Keep Plain Text")])),
|
||||
// Appearance of Black
|
||||
p!("blackOnScreen", "Appearance of Black", "", "On Screen", choice(BLACK)),
|
||||
p!("blackOutput", "Appearance of Black", "", "Printing / Exporting", choice(BLACK_OUT)),
|
||||
// Devices
|
||||
p!("touchWorkspace", "Devices", "", "Enable Touch Workspace", bool),
|
||||
p!("touchGestures", "Devices", "", "Enable Touch Gestures", bool),
|
||||
];
|
||||
|
||||
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
|
||||
PREF_SPECS.iter().find(|s| s.key == key)
|
||||
}
|
||||
|
||||
/// Validate (and normalize) a value for `key`. Numbers may be given as strings; booleans as
|
||||
/// "true"/"false"; choices by value or label (case-insensitive).
|
||||
pub fn validate(key: &str, v: &Value) -> std::result::Result<Value, String> {
|
||||
let sp = spec(key).ok_or_else(|| format!("unknown preference `{key}`"))?;
|
||||
let num = || v.as_f64().or_else(|| v.as_str().and_then(|s| s.trim().trim_end_matches(|c: char| c.is_alphabetic() || c == '°').trim().parse().ok()));
|
||||
match sp.kind {
|
||||
PrefKind::Bool => match v {
|
||||
Value::Bool(b) => Ok(json!(b)),
|
||||
Value::String(s) if s == "true" || s == "false" => Ok(json!(s == "true")),
|
||||
_ => Err(format!("`{key}` must be true or false")),
|
||||
},
|
||||
PrefKind::Num { min, max, .. } => match num() {
|
||||
Some(x) if x.is_finite() && (min..=max).contains(&x) => Ok(json!(x)),
|
||||
Some(x) => Err(format!("`{key}` must be between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a number")),
|
||||
},
|
||||
PrefKind::Int { min, max } => match num() {
|
||||
Some(x) if x.fract() == 0.0 && (min as f64..=max as f64).contains(&x) => Ok(json!(x as i64)),
|
||||
Some(x) => Err(format!("`{key}` must be a whole number between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a whole number")),
|
||||
},
|
||||
PrefKind::Choice(opts) => {
|
||||
let s = v.as_str().ok_or_else(|| format!("`{key}` must be a string"))?;
|
||||
opts.iter()
|
||||
.find(|(val, label)| val.eq_ignore_ascii_case(s) || label.eq_ignore_ascii_case(s))
|
||||
.map(|(val, _)| json!(val))
|
||||
.ok_or_else(|| format!("`{key}` must be one of {}", opts.iter().map(|o| o.0).collect::<Vec<_>>().join(", ")))
|
||||
}
|
||||
PrefKind::Color => {
|
||||
let s = v.as_str().unwrap_or("");
|
||||
let hex = s.trim_start_matches('#');
|
||||
if hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
Ok(json!(format!("#{}", hex.to_ascii_lowercase())))
|
||||
} else {
|
||||
Err(format!("`{key}` must be a #rrggbb colour"))
|
||||
}
|
||||
}
|
||||
PrefKind::Text => match v {
|
||||
Value::String(s) => Ok(json!(s)),
|
||||
_ => Err(format!("`{key}` must be a string")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefs {
|
||||
pub fn to_json(&self) -> Value {
|
||||
serde_json::to_value(self).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
/// Set several validated values at once; on any error nothing changes.
|
||||
pub fn set_values(&mut self, values: &Map<String, Value>) -> std::result::Result<(), String> {
|
||||
let mut obj = self.to_json();
|
||||
for (k, v) in values {
|
||||
obj[k.as_str()] = validate(k, v)?;
|
||||
}
|
||||
*self = serde_json::from_value(obj).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Values of one category (`None` = all) reset to defaults.
|
||||
pub fn reset(&mut self, category: Option<&str>) {
|
||||
let d = Prefs::default().to_json();
|
||||
let mut obj = self.to_json();
|
||||
for sp in PREF_SPECS.iter().filter(|s| category.is_none_or(|c| c.eq_ignore_ascii_case(s.category))) {
|
||||
obj[sp.key] = d[sp.key].clone();
|
||||
}
|
||||
if let Ok(p) = serde_json::from_value(obj) {
|
||||
*self = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Replace the preferences and push the ones with engine-side consumers into open documents
|
||||
/// and the renderer (grid, history depth, render threads).
|
||||
pub fn apply_prefs(&mut self, p: Prefs) {
|
||||
let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;
|
||||
let history_changed = p.history_states != self.prefs.history_states;
|
||||
self.prefs = p;
|
||||
drawcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok());
|
||||
for st in &mut self.docs {
|
||||
if history_changed {
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
let over = st.history.undo.len().saturating_sub(st.history.limit);
|
||||
st.history.undo.drain(..over);
|
||||
}
|
||||
if grid_changed {
|
||||
let d = std::sync::Arc::make_mut(&mut st.doc);
|
||||
d.grid.spacing = self.prefs.gridline_every;
|
||||
d.grid.subdivisions = self.prefs.grid_subdivisions;
|
||||
st.revision += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "prefs.get", "Get Preferences", [], None, "{key?} → the value of one preference, or {prefs: {…all…}}", always, get),
|
||||
cmd!(
|
||||
query "prefs.set",
|
||||
"Set Preference",
|
||||
[],
|
||||
None,
|
||||
"{key, value} or {values: {key: value, …}} set preferences (validated; see prefs.list)",
|
||||
always,
|
||||
set
|
||||
),
|
||||
cmd!(query "prefs.reset", "Reset Preferences", [], None, "{category?} reset one category (or everything) to defaults", always, reset),
|
||||
cmd!(query "prefs.list", "List Preferences", [], None, "{} → [{key, category, section, label, kind, value, …}] every preference", always, list),
|
||||
]
|
||||
}
|
||||
|
||||
fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let all = s.prefs.to_json();
|
||||
match str_param(p, "key") {
|
||||
Some(k) if spec(k).is_some() => Ok(all[k].clone()),
|
||||
Some(k) => Err(bad("prefs.get", format!("unknown preference `{k}`"))),
|
||||
None => Ok(json!({ "prefs": all })),
|
||||
}
|
||||
}
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut values = p.get("values").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
if let Some(k) = str_param(p, "key") {
|
||||
values.insert(k.to_string(), p.get("value").cloned().ok_or_else(|| bad("prefs.set", "missing `value`"))?);
|
||||
}
|
||||
if values.is_empty() {
|
||||
return Err(bad("prefs.set", "give {key, value} or {values}"));
|
||||
}
|
||||
let mut next = s.prefs.clone();
|
||||
next.set_values(&values).map_err(|e| bad("prefs.set", e))?;
|
||||
s.apply_prefs(next);
|
||||
let all = s.prefs.to_json();
|
||||
Ok(Value::Object(values.keys().map(|k| (k.clone(), all[k.as_str()].clone())).collect()))
|
||||
}
|
||||
|
||||
fn reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cat = str_param(p, "category");
|
||||
if let Some(c) = cat
|
||||
&& !PREF_CATEGORIES.iter().any(|x| x.eq_ignore_ascii_case(c))
|
||||
{
|
||||
return Err(bad("prefs.reset", format!("unknown category `{c}`")));
|
||||
}
|
||||
let mut next = s.prefs.clone();
|
||||
next.reset(cat);
|
||||
s.apply_prefs(next);
|
||||
Ok(json!({ "prefs": s.prefs.to_json() }))
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let all = s.prefs.to_json();
|
||||
Ok(Value::Array(
|
||||
PREF_SPECS
|
||||
.iter()
|
||||
.map(|sp| {
|
||||
let mut o = json!({"key": sp.key, "category": sp.category, "section": sp.section, "label": sp.label, "value": all[sp.key]});
|
||||
match sp.kind {
|
||||
PrefKind::Bool => o["kind"] = json!("bool"),
|
||||
PrefKind::Num { min, max, unit } => {
|
||||
o["kind"] = json!("number");
|
||||
o["min"] = json!(min);
|
||||
o["max"] = json!(max);
|
||||
o["unit"] = json!(unit);
|
||||
}
|
||||
PrefKind::Int { min, max } => {
|
||||
o["kind"] = json!("integer");
|
||||
o["min"] = json!(min);
|
||||
o["max"] = json!(max);
|
||||
}
|
||||
PrefKind::Choice(c) => {
|
||||
o["kind"] = json!("choice");
|
||||
o["options"] = json!(c.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||
}
|
||||
PrefKind::Color => o["kind"] = json!("color"),
|
||||
PrefKind::Text => o["kind"] = json!("text"),
|
||||
}
|
||||
o
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
+254
-5
@@ -154,21 +154,249 @@ pub enum DrawMode {
|
||||
Inside,
|
||||
}
|
||||
|
||||
/// Application preferences that affect commands.
|
||||
#[derive(Clone, Debug)]
|
||||
/// Application preferences (Edit → Preferences). Every field is reachable through `prefs.get` /
|
||||
/// `prefs.set {key, value}` (camelCase keys, validated against [`cmd::prefscmds::PREF_SPECS`]).
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Prefs {
|
||||
// General
|
||||
/// Arrow-key nudge distance (General → Keyboard Increment), points.
|
||||
pub keyboard_increment: f64,
|
||||
pub constrain_angle: f64,
|
||||
pub corner_radius: f64,
|
||||
pub disable_auto_add_delete: bool,
|
||||
pub use_precise_cursors: bool,
|
||||
pub show_tool_tips: bool,
|
||||
pub anti_aliased_artwork: bool,
|
||||
pub select_same_tint_percent: bool,
|
||||
pub show_home_screen: bool,
|
||||
pub use_preview_bounds: bool,
|
||||
pub display_print_size: bool,
|
||||
pub double_click_to_isolate: bool,
|
||||
pub transform_pattern_tiles: bool,
|
||||
pub scale_corners: bool,
|
||||
/// Scale Strokes & Effects.
|
||||
pub scale_strokes: bool,
|
||||
pub zoom_with_mouse_wheel: bool,
|
||||
/// Offset for Paste / duplicate (Illustrator pastes to the view centre; we offset by this).
|
||||
pub paste_offset: f64,
|
||||
pub corner_radius: f64,
|
||||
// Selection & Anchor Display
|
||||
pub selection_tolerance: f64,
|
||||
pub object_selection_by_path_only: bool,
|
||||
pub snap_to_point_tolerance: f64,
|
||||
pub ctrl_click_selects_behind: bool,
|
||||
pub zoom_to_selection: bool,
|
||||
pub move_locked_with_artboard: bool,
|
||||
pub anchor_size: u32,
|
||||
pub handle_style: String,
|
||||
pub highlight_anchors_on_hover: bool,
|
||||
pub show_handles_multiple_anchors: bool,
|
||||
pub hide_corner_widget_above: f64,
|
||||
pub pen_rubber_band: bool,
|
||||
pub curvature_rubber_band: bool,
|
||||
// Type
|
||||
pub type_size_increment: f64,
|
||||
pub tracking_increment: f64,
|
||||
pub baseline_shift_increment: f64,
|
||||
pub show_east_asian_options: bool,
|
||||
pub show_indic_options: bool,
|
||||
pub type_selection_by_path_only: bool,
|
||||
pub font_names_in_english: bool,
|
||||
pub auto_size_area_type: bool,
|
||||
pub font_preview: bool,
|
||||
pub font_preview_size: String,
|
||||
pub recent_fonts_count: u32,
|
||||
pub missing_glyph_protection: bool,
|
||||
pub highlight_alternate_glyphs: bool,
|
||||
pub placeholder_text: bool,
|
||||
// Units
|
||||
pub units_general: String,
|
||||
pub units_stroke: String,
|
||||
pub units_type: String,
|
||||
pub units_asian_type: String,
|
||||
pub numbers_without_units_are_points: bool,
|
||||
pub identify_objects_by: String,
|
||||
// Guides & Grid
|
||||
pub guide_color: String,
|
||||
pub guide_style: String,
|
||||
pub grid_color: String,
|
||||
pub grid_style: String,
|
||||
pub gridline_every: f64,
|
||||
pub grid_subdivisions: u32,
|
||||
pub grids_in_back: bool,
|
||||
pub show_pixel_grid: bool,
|
||||
// Smart Guides
|
||||
pub smart_guide_color: String,
|
||||
pub alignment_guides: bool,
|
||||
pub object_highlighting: bool,
|
||||
pub transform_tools_guides: bool,
|
||||
pub construction_guides: bool,
|
||||
pub construction_angles: String,
|
||||
pub anchor_path_labels: bool,
|
||||
pub measurement_labels: bool,
|
||||
pub spacing_guides: bool,
|
||||
pub snapping_tolerance: f64,
|
||||
// Slices
|
||||
pub show_slice_numbers: bool,
|
||||
pub slice_line_color: String,
|
||||
// Hyphenation
|
||||
pub hyphenation_language: String,
|
||||
pub hyphenation_exceptions: String,
|
||||
// Performance & Storage (Plug-ins & Scratch Disks)
|
||||
pub plugins_folder: String,
|
||||
pub scratch_primary: String,
|
||||
pub scratch_secondary: String,
|
||||
// User Interface
|
||||
pub ui_brightness: String,
|
||||
pub canvas_color: String,
|
||||
pub auto_collapse_icon_panels: bool,
|
||||
pub open_documents_as_tabs: bool,
|
||||
pub large_tabs: bool,
|
||||
pub ui_scaling: f64,
|
||||
pub scale_cursor_with_ui: bool,
|
||||
// Performance
|
||||
pub gpu_performance: bool,
|
||||
pub animated_zoom: bool,
|
||||
pub history_states: u32,
|
||||
pub real_time_drawing: bool,
|
||||
/// Rasterizer worker threads; -1 = automatic.
|
||||
pub render_threads: i32,
|
||||
// File Handling
|
||||
pub background_save: bool,
|
||||
pub background_export: bool,
|
||||
pub autosave_recovery: bool,
|
||||
pub autosave_interval: u32,
|
||||
pub recovery_folder: String,
|
||||
pub recovery_off_for_complex: bool,
|
||||
pub recent_files_count: u32,
|
||||
pub low_res_proxy_eps: bool,
|
||||
pub anti_aliased_bitmaps: bool,
|
||||
pub update_links: String,
|
||||
// Clipboard Handling
|
||||
pub copy_as_svg: bool,
|
||||
pub copy_as_pdf: bool,
|
||||
pub copy_aicb: bool,
|
||||
pub aicb_mode: String,
|
||||
pub paste_text_formatting: String,
|
||||
// Appearance of Black
|
||||
pub black_on_screen: String,
|
||||
pub black_output: String,
|
||||
// Devices
|
||||
pub touch_workspace: bool,
|
||||
pub touch_gestures: bool,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
fn default() -> Self {
|
||||
Self { keyboard_increment: 1.0, scale_strokes: true, paste_offset: 10.0, corner_radius: 12.0 }
|
||||
let s = |v: &str| v.to_string();
|
||||
Self {
|
||||
keyboard_increment: 1.0,
|
||||
constrain_angle: 0.0,
|
||||
corner_radius: 12.0,
|
||||
disable_auto_add_delete: false,
|
||||
use_precise_cursors: false,
|
||||
show_tool_tips: true,
|
||||
anti_aliased_artwork: true,
|
||||
select_same_tint_percent: false,
|
||||
show_home_screen: true,
|
||||
use_preview_bounds: false,
|
||||
display_print_size: false,
|
||||
double_click_to_isolate: true,
|
||||
transform_pattern_tiles: false,
|
||||
scale_corners: false,
|
||||
scale_strokes: true,
|
||||
zoom_with_mouse_wheel: false,
|
||||
paste_offset: 10.0,
|
||||
selection_tolerance: 3.0,
|
||||
object_selection_by_path_only: false,
|
||||
snap_to_point_tolerance: 2.0,
|
||||
ctrl_click_selects_behind: true,
|
||||
zoom_to_selection: true,
|
||||
move_locked_with_artboard: false,
|
||||
anchor_size: 3,
|
||||
handle_style: s("solid"),
|
||||
highlight_anchors_on_hover: true,
|
||||
show_handles_multiple_anchors: true,
|
||||
hide_corner_widget_above: 177.0,
|
||||
pen_rubber_band: true,
|
||||
curvature_rubber_band: true,
|
||||
type_size_increment: 2.0,
|
||||
tracking_increment: 20.0,
|
||||
baseline_shift_increment: 2.0,
|
||||
show_east_asian_options: false,
|
||||
show_indic_options: false,
|
||||
type_selection_by_path_only: false,
|
||||
font_names_in_english: true,
|
||||
auto_size_area_type: false,
|
||||
font_preview: true,
|
||||
font_preview_size: s("medium"),
|
||||
recent_fonts_count: 10,
|
||||
missing_glyph_protection: true,
|
||||
highlight_alternate_glyphs: true,
|
||||
placeholder_text: true,
|
||||
units_general: s("points"),
|
||||
units_stroke: s("points"),
|
||||
units_type: s("points"),
|
||||
units_asian_type: s("points"),
|
||||
numbers_without_units_are_points: true,
|
||||
identify_objects_by: s("objectName"),
|
||||
guide_color: s("#4affff"),
|
||||
guide_style: s("lines"),
|
||||
grid_color: s("#c8c8c8"),
|
||||
grid_style: s("lines"),
|
||||
gridline_every: 72.0,
|
||||
grid_subdivisions: 8,
|
||||
grids_in_back: true,
|
||||
show_pixel_grid: true,
|
||||
smart_guide_color: s("#ff4af0"),
|
||||
alignment_guides: true,
|
||||
object_highlighting: true,
|
||||
transform_tools_guides: true,
|
||||
construction_guides: true,
|
||||
construction_angles: s("90° & 45° Angles"),
|
||||
anchor_path_labels: true,
|
||||
measurement_labels: true,
|
||||
spacing_guides: true,
|
||||
snapping_tolerance: 4.0,
|
||||
show_slice_numbers: true,
|
||||
slice_line_color: s("#ff3f3f"),
|
||||
hyphenation_language: s("English: USA"),
|
||||
hyphenation_exceptions: String::new(),
|
||||
plugins_folder: String::new(),
|
||||
scratch_primary: s("Startup"),
|
||||
scratch_secondary: s("None"),
|
||||
ui_brightness: s("mediumDark"),
|
||||
canvas_color: s("matchUi"),
|
||||
auto_collapse_icon_panels: false,
|
||||
open_documents_as_tabs: true,
|
||||
large_tabs: false,
|
||||
ui_scaling: 1.0,
|
||||
scale_cursor_with_ui: false,
|
||||
gpu_performance: true,
|
||||
animated_zoom: true,
|
||||
history_states: 500,
|
||||
real_time_drawing: true,
|
||||
render_threads: -1,
|
||||
background_save: true,
|
||||
background_export: true,
|
||||
autosave_recovery: true,
|
||||
autosave_interval: 2,
|
||||
recovery_folder: String::new(),
|
||||
recovery_off_for_complex: false,
|
||||
recent_files_count: 20,
|
||||
low_res_proxy_eps: false,
|
||||
anti_aliased_bitmaps: false,
|
||||
update_links: s("askWhenModified"),
|
||||
copy_as_svg: true,
|
||||
copy_as_pdf: false,
|
||||
copy_aicb: false,
|
||||
aicb_mode: s("preserveAppearance"),
|
||||
paste_text_formatting: s("keep"),
|
||||
black_on_screen: s("accurate"),
|
||||
black_output: s("accurate"),
|
||||
touch_workspace: true,
|
||||
touch_gestures: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,8 +468,23 @@ impl Session {
|
||||
pub fn doc_mut(&mut self) -> Result<&mut DocState> {
|
||||
self.active_mut().ok_or(EngineError::NoDocument)
|
||||
}
|
||||
/// Finish the tool's pending work in the current document and give it fresh state, so nothing
|
||||
/// (e.g. uncommitted typing) leaks into the document we are switching to.
|
||||
fn reset_tool_for_doc_switch(&mut self) {
|
||||
if self.active.is_none() {
|
||||
return;
|
||||
}
|
||||
let view = self.last_view;
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
let _ = self.apply_actions(acts);
|
||||
let _ = self.cancel_interaction();
|
||||
self.tool = drawcraft_tools::create(self.tool.id());
|
||||
}
|
||||
pub fn set_active(&mut self, index: usize) -> bool {
|
||||
if index < self.docs.len() {
|
||||
if self.active != Some(index) {
|
||||
self.reset_tool_for_doc_switch();
|
||||
}
|
||||
self.active = Some(index);
|
||||
true
|
||||
} else {
|
||||
@@ -254,7 +497,10 @@ impl Session {
|
||||
}
|
||||
/// Add a document and make it active.
|
||||
pub fn add_document(&mut self, doc: Document, path: Option<String>) -> usize {
|
||||
self.docs.push(DocState::new(doc, path));
|
||||
self.reset_tool_for_doc_switch();
|
||||
let mut st = DocState::new(doc, path);
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
self.docs.push(st);
|
||||
let i = self.docs.len() - 1;
|
||||
self.active = Some(i);
|
||||
i
|
||||
@@ -263,6 +509,7 @@ impl Session {
|
||||
if index >= self.docs.len() {
|
||||
return false;
|
||||
}
|
||||
self.reset_tool_for_doc_switch();
|
||||
self.docs.remove(index);
|
||||
self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) };
|
||||
true
|
||||
@@ -423,6 +670,8 @@ mod tests_menucmds;
|
||||
#[cfg(test)]
|
||||
mod tests_panelcmds;
|
||||
#[cfg(test)]
|
||||
mod tests_prefs;
|
||||
#[cfg(test)]
|
||||
mod tests_pathops;
|
||||
#[cfg(test)]
|
||||
mod tests_textedit;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Preferences: `prefs.get` / `prefs.set` / `prefs.reset` / `prefs.list`.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::cmd::prefscmds::{PREF_CATEGORIES, PREF_SPECS, validate};
|
||||
|
||||
#[test]
|
||||
fn every_spec_matches_a_prefs_field_and_back() {
|
||||
let all = Prefs::default().to_json();
|
||||
let obj = all.as_object().unwrap();
|
||||
for sp in PREF_SPECS {
|
||||
assert!(obj.contains_key(sp.key), "spec `{}` has no Prefs field", sp.key);
|
||||
assert!(PREF_CATEGORIES.contains(&sp.category), "spec `{}` has unknown category", sp.key);
|
||||
// Defaults validate against their own spec.
|
||||
assert!(validate(sp.key, &obj[sp.key]).is_ok(), "default of `{}` fails validation", sp.key);
|
||||
}
|
||||
for k in obj.keys() {
|
||||
assert!(PREF_SPECS.iter().any(|s| s.key == k), "Prefs field `{k}` has no spec");
|
||||
}
|
||||
for c in PREF_CATEGORIES {
|
||||
assert!(PREF_SPECS.iter().any(|s| s.category == *c), "category {c} is empty");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_keyboard_increment_drives_nudge() {
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", &json!({"key": "keyboardIncrement", "value": 5})).unwrap();
|
||||
assert_eq!(s.execute("prefs.get", &json!({"key": "keyboardIncrement"})).unwrap(), json!(5.0));
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
s.execute("object.nudge", &json!({"dx": 1, "dy": 0})).unwrap();
|
||||
let b = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap();
|
||||
assert!((b.x0 - 15.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_rejects_out_of_range_and_wrong_types() {
|
||||
let mut s = Session::new();
|
||||
assert!(s.execute("prefs.set", &json!({"key": "keyboardIncrement", "value": -1})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "anchorSize", "value": 9})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "anchorSize", "value": 2.5})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "scaleStrokes", "value": 3})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "uiBrightness", "value": "purple"})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "gridColor", "value": "#12345"})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "nope", "value": 1})).is_err());
|
||||
assert!(s.execute("prefs.set", &json!({"key": "scaleStrokes"})).is_err());
|
||||
assert_eq!(s.prefs, Prefs::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_normalizes_strings_labels_and_colours() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("prefs.set", &json!({"values": {"uiBrightness": "Medium Light", "gridColor": "ABCDEF", "keyboardIncrement": "2.5 pt", "scaleStrokes": "false"}})).unwrap();
|
||||
assert_eq!(r["uiBrightness"], json!("mediumLight"));
|
||||
assert_eq!(s.prefs.grid_color, "#abcdef");
|
||||
assert_eq!(s.prefs.keyboard_increment, 2.5);
|
||||
assert!(!s.prefs.scale_strokes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_set_is_atomic() {
|
||||
let mut s = Session::new();
|
||||
assert!(s.execute("prefs.set", &json!({"values": {"keyboardIncrement": 3, "anchorSize": 99}})).is_err());
|
||||
assert_eq!(s.prefs.keyboard_increment, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_prefs_apply_to_open_documents() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
s.execute("prefs.set", &json!({"values": {"gridlineEvery": 36, "gridSubdivisions": 4}})).unwrap();
|
||||
let g = &s.doc().unwrap().doc.grid;
|
||||
assert_eq!((g.spacing, g.subdivisions), (36.0, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_states_limit_undo_depth() {
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", &json!({"key": "historyStates", "value": 5})).unwrap();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
for i in 0..9 {
|
||||
s.execute("shape.rectangle", &json!({"x": i * 10, "y": 0, "width": 5, "height": 5})).unwrap();
|
||||
}
|
||||
assert!(s.doc().unwrap().history.undo.len() <= 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_category_and_all() {
|
||||
let mut s = Session::new();
|
||||
s.execute("prefs.set", &json!({"values": {"keyboardIncrement": 7, "gridColor": "#000000"}})).unwrap();
|
||||
s.execute("prefs.reset", &json!({"category": "General"})).unwrap();
|
||||
assert_eq!(s.prefs.keyboard_increment, 1.0);
|
||||
assert_eq!(s.prefs.grid_color, "#000000");
|
||||
s.execute("prefs.reset", &json!({})).unwrap();
|
||||
assert_eq!(s.prefs, Prefs::default());
|
||||
assert!(s.execute("prefs.reset", &json!({"category": "Bogus"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_describes_every_pref() {
|
||||
let mut s = Session::new();
|
||||
let l = s.execute("prefs.list", &json!({})).unwrap();
|
||||
let a = l.as_array().unwrap();
|
||||
assert_eq!(a.len(), PREF_SPECS.len());
|
||||
assert!(a.iter().any(|e| e["key"] == "renderThreads" && e["kind"] == "integer" && e["min"] == -1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefs_serde_round_trip_and_tolerates_missing_fields() {
|
||||
let mut p = Prefs::default();
|
||||
p.ui_scaling = 1.25;
|
||||
p.units_general = "millimeters".into();
|
||||
let back: Prefs = serde_json::from_value(p.to_json()).unwrap();
|
||||
assert_eq!(back, p);
|
||||
let partial: Prefs = serde_json::from_value(json!({"keyboardIncrement": 4})).unwrap();
|
||||
assert_eq!(partial.keyboard_increment, 4.0);
|
||||
assert_eq!(partial.corner_radius, 12.0);
|
||||
}
|
||||
@@ -321,3 +321,22 @@ fn commands_are_listed_with_params() {
|
||||
assert!(cmds.iter().any(|c| c["id"] == id), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_tool_state_does_not_leak_across_documents() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 200.0, 200.0);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
s.tool_text("First", v).unwrap();
|
||||
// A second document whose first object reuses the same id.
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let id2 = text(&mut s, "Other");
|
||||
assert!(!s.tool_wants_text(), "switching documents ends the edit");
|
||||
s.tool_text("XYZ", v).unwrap();
|
||||
assert_eq!(obj(&s, id2).plain_text(), "Other");
|
||||
s.set_active(0);
|
||||
assert_eq!(obj(&s, id).plain_text(), "First");
|
||||
assert!(!s.in_interaction());
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ impl Session {
|
||||
if self.tool.id() == id {
|
||||
return Ok(());
|
||||
}
|
||||
let acts = self.with_cx(view, |t, cx| t.deactivate(cx));
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
self.apply_actions(acts)?;
|
||||
self.tool = drawcraft_tools::create(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn with_cx<R>(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn drawcraft_tools::Tool, &ToolContext) -> R) -> R
|
||||
pub(crate) fn with_tool_cx<R>(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn drawcraft_tools::Tool, &ToolContext) -> R) -> R
|
||||
where
|
||||
R: Default,
|
||||
{
|
||||
@@ -67,18 +67,18 @@ impl Session {
|
||||
/// Feed a pointer event to the active tool. Returns requests for the UI.
|
||||
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
self.last_view = view;
|
||||
let acts = self.with_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
self.apply_actions(acts)
|
||||
}
|
||||
|
||||
pub fn tool_key(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
let acts = self.with_cx(view, |t, cx| t.key(cx, key, mods));
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.key(cx, key, mods));
|
||||
self.apply_actions(acts)
|
||||
}
|
||||
|
||||
/// Typed text for the active tool (Type tool).
|
||||
pub fn tool_text(&mut self, text: &str, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
let acts = self.with_cx(view, |t, cx| t.text_input(cx, text));
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.text_input(cx, text));
|
||||
self.apply_actions(acts)
|
||||
}
|
||||
|
||||
@@ -91,11 +91,11 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn overlays(&mut self, view: ViewInfo) -> Vec<Overlay> {
|
||||
self.with_cx(view, |t, cx| t.overlays(cx))
|
||||
self.with_tool_cx(view, |t, cx| t.overlays(cx))
|
||||
}
|
||||
|
||||
pub fn cursor(&mut self, p: Point, mods: Mods, view: ViewInfo) -> Cursor {
|
||||
self.with_cx(view, |t, cx| t.cursor(cx, p, mods))
|
||||
self.with_tool_cx(view, |t, cx| t.cursor(cx, p, mods))
|
||||
}
|
||||
|
||||
pub fn tool_options(&self) -> Value {
|
||||
@@ -125,7 +125,7 @@ impl Session {
|
||||
Action::SwitchTool(t) => ui.push(UiRequest::SwitchTool(t)),
|
||||
Action::Notify(what) => {
|
||||
let view = self.last_view;
|
||||
self.with_cx(view, |t, cx| t.notify(cx, &what));
|
||||
self.with_tool_cx(view, |t, cx| t.notify(cx, &what));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user