Merge pull request #1663 from ham9zah/pen-drag-threshold

Pen: drag threshold, Precise Cursors command and tests
This commit is contained in:
BFlat authored and GitHub committed 2026-10-14 04:28:59 +07:00
commit f8396dbc4f
20 files changed
+325 -1

No files matched your search

+128
View File
@@ -373,6 +373,134 @@ fn use_precise_cursors_makes_drawing_cursors_crosshairs() {
assert_eq!(cursor(&mut s, "eyedropper"), Cursor::Eyedropper);
}
/// Use Precise Cursors changes the pointer of the tools that have one of their own: off, each shows its own
/// pointer; on, a crosshair. Every other drawing tool shows a crosshair either way, and the Type tool and the
/// Scissors over no path keep theirs.
#[test]
fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
use vectorcraft_tools::{Cursor, Mods};
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let at = vectorcraft_geom::Point::new(300.0, 300.0);
let cursor = |s: &mut Session, tool: &str| {
s.select_tool(tool, ViewInfo::default()).unwrap();
s.cursor(at, Mods::default(), ViewInfo::default())
};
// Off: the pointer of each tool whose own pointer the preference replaces.
let own = [
("pen", Cursor::Pen),
("addAnchor", Cursor::PenAdd),
("deleteAnchor", Cursor::PenDelete),
("anchorPoint", Cursor::Pen),
("curvature", Cursor::Pen),
("mesh", Cursor::PenAdd),
("eyedropper", Cursor::Eyedropper),
("slice", Cursor::Slice),
("blend", Cursor::Blend),
("shapeBuilder", Cursor::ShapeBuilder),
];
for (tool, c) in own {
assert_eq!(cursor(&mut s, tool), c, "{tool} off");
}
// The tools that show a crosshair whatever the preference says.
let always = [
"lineSegment",
"arc",
"spiral",
"rectangularGrid",
"polarGrid",
"rectangle",
"roundedRectangle",
"ellipse",
"polygon",
"star",
"flare",
"paintbrush",
"blobBrush",
"shaper",
"pencil",
"smooth",
"pathEraser",
"join",
"eraser",
"knife",
"mirrorCut",
"lineCut",
"rectCut",
"gradient",
];
for tool in always {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} off");
}
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
for (tool, _) in own {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
}
for tool in always {
assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on");
}
assert_eq!(cursor(&mut s, "type"), Cursor::Text);
assert_eq!(cursor(&mut s, "scissors"), Cursor::NotAllowed);
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": false})).unwrap();
for (tool, c) in own {
assert_eq!(cursor(&mut s, tool), c, "{tool} off again");
}
}
/// Every state of the Pen's pointer, and the other drawing pointers, turn into a crosshair; the rest stay.
#[test]
fn precise_cursors_replace_every_pen_state() {
use vectorcraft_tools::Cursor;
for c in [
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenContinue,
Cursor::PenJoin,
Cursor::PenConvert,
Cursor::Eyedropper,
Cursor::Slice,
Cursor::Blend,
Cursor::BlendObject,
Cursor::BlendAnchor,
Cursor::ShapeBuilder,
Cursor::ShapeBuilderErase,
] {
assert_eq!(c.precise(), Cursor::Crosshair, "{c:?}");
}
for c in [Cursor::Arrow, Cursor::Move, Cursor::Text, Cursor::Hand, Cursor::NotAllowed, Cursor::SliceSelect] {
assert_eq!(c.precise(), c, "{c:?}");
}
}
/// Switching the preference is not an edit: no undo step, no unsaved changes.
#[test]
fn use_precise_cursors_is_not_an_edit() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let before = (s.active().unwrap().history.undo.len(), s.active().unwrap().history.redo.len());
assert!(!s.active().unwrap().is_dirty());
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
assert!(s.prefs.use_precise_cursors);
let st = s.active().unwrap();
assert_eq!((st.history.undo.len(), st.history.redo.len()), before, "no undo step");
assert!(!st.is_dirty(), "the document stays saved");
}
/// The preference is saved with the others and reads back as it was.
#[test]
fn use_precise_cursors_is_saved_and_restored() {
let mut s = Session::new();
assert!(!s.prefs.use_precise_cursors, "off by default");
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap();
let saved = s.prefs.to_json();
assert_eq!(saved["usePreciseCursors"], json!(true));
let mut t = Session::new();
t.apply_prefs(serde_json::from_value(saved).unwrap());
assert!(t.prefs.use_precise_cursors);
}
// ---------- #394, second batch ----------
fn new_doc() -> Session {
@@ -0,0 +1,37 @@
//! A click after a smooth anchor: the new corner has no handles, so the segment curves from the previous anchor's side only.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use serde_json::json;
use vectorcraft_doc::NodeKind;
use vectorcraft_engine::{Session, ViewInfo};
use vectorcraft_geom::Point;
use vectorcraft_tools::{PointerEvent, PointerKind};
#[test]
fn click_after_a_smooth_anchor_curves_from_its_side_only() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
let view = ViewInfo { zoom: 1.0, smart_guides: false, snap_to_grid: false, snap_to_pixel: false, snap_to_point: false, ..Default::default() };
s.select_tool("pen", view).unwrap();
let mut at = |kind, x, y| s.pointer(&PointerEvent::new(kind, x, y), view).unwrap();
// Press and drag out a smooth anchor at (100, 100), then click a corner at (300, 100).
at(PointerKind::Down, 100.0, 100.0);
at(PointerKind::Drag, 150.0, 60.0);
at(PointerKind::Up, 150.0, 60.0);
at(PointerKind::Down, 300.0, 100.0);
at(PointerKind::Up, 300.0, 100.0);
let mut path = None;
s.doc().unwrap().doc.walk(|n| {
if let NodeKind::Path { path: p, .. } = &n.kind {
path = Some(p.clone());
}
});
let sp = &path.unwrap().subpaths[0];
let (a, b) = (&sp.anchors[0], &sp.anchors[1]);
assert_eq!(a.h_out, Point::new(150.0, 60.0), "the outgoing handle stays where the drag left it");
assert_eq!(a.h_in, Point::new(50.0, 140.0), "mirrored through the anchor");
assert!(!b.has_in() && !b.has_out(), "a click places a corner without handles");
assert!(!sp.segment_is_line(0), "the segment is a curve, shaped by the previous anchor's handle only");
assert_eq!(sp.segment(0).p2, b.p);
}
+53
View File
@@ -44,6 +44,11 @@ pub struct PenTool {
in_h: Point,
/// The pointer at the last button-down or drag: Space held moves the anchor as far as it moves.
last: Point,
/// Where the button went down: the drag threshold is measured from it.
pressed: Point,
/// The pointer has moved past the drag threshold since the press: a press released inside it
/// is a click, so the anchor stays a corner without handles.
dragged: bool,
/// The last anchor of the path being drawn, while a click retracts its outgoing handle or a
/// drag pulls a new one out: (path, subpath, anchor, its position).
handle: Option<(NodeId, usize, usize, Point)>,
@@ -53,6 +58,9 @@ pub struct PenTool {
snap: DrawSnap,
}
/// Screen pixels the pointer must move after a press before it shapes handles instead of clicking.
const DRAG_THRESHOLD: f64 = 3.0;
/// An anchor being placed, as the press decided: where it is, the path it goes on (none: the
/// first anchor of a new path) and whether it closes that path (the press was on its first
/// anchor). The drag only shapes its handles, whatever the path looks like meanwhile: a closing
@@ -181,6 +189,8 @@ impl Tool for PenTool {
}
PointerKind::Down => {
self.last = ev.pos;
self.pressed = ev.pos;
self.dragged = false;
if let Some((id, first, last, _)) = active {
let end = last_anchor(cx, id);
let closing_end = end.filter(|(_, ai)| *ai > 0).map(|_| first);
@@ -265,6 +275,16 @@ impl Tool for PenTool {
if let Some(t) = &mut self.convert {
return t.pointer(cx, ev);
}
// Illustrator treats a press as a drag only past a few screen pixels. `last` follows the
// pointer meanwhile, so Space pressed before the threshold moves the anchor by the
// next move only.
if !self.dragged {
if ev.pos.distance(self.pressed) < cx.tol(DRAG_THRESHOLD) {
self.last = ev.pos;
return vec![];
}
self.dragged = true;
}
if let Some((id, si, ai, a)) = self.handle {
let mut h = ev.pos;
if ev.mods.shift {
@@ -466,6 +486,20 @@ mod tests {
use crate::testutil::*;
use vectorcraft_doc::Selection;
#[test]
fn press_inside_drag_threshold_places_a_corner() {
let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());
let paint = paint();
let cx =
ToolContext { zoom: 1.0, smart_guides: false, snap_to_point: false, snap_to_pixel: false, snap_to_grid: false, ..cx(&d, &s, &paint) };
let mut pen = PenTool::default();
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
assert!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 101.0, 101.0)).is_empty());
assert_eq!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Up, 101.0, 101.0)), vec![Action::Commit]);
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 200.0));
assert_eq!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 204.0, 200.0)).len(), 1);
}
#[test]
fn snapped_last_endpoint_converts_active_path_after_raw_miss() {
let first = Point::new(100.0, 300.0);
@@ -677,6 +711,25 @@ mod tests {
assert_eq!(handles(t.pointer(&cx, &drag)), (json!([300.0, 440.0]), json!([340.0, 440.0])));
}
#[test]
fn space_pressed_inside_the_drag_threshold_moves_the_anchor_by_the_next_move_only() {
let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());
let paint = paint();
let cx =
ToolContext { zoom: 1.0, smart_guides: false, snap_to_point: false, snap_to_pixel: false, snap_to_grid: false, ..cx(&d, &s, &paint) };
let space = Mods { space: true, ..Mods::default() };
let mut pen = PenTool::default();
pen.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
// Still inside the threshold, Space held: nothing happens yet.
assert!(pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 102.0, 100.0).with_mods(space)).is_empty());
// The move that crosses it moves the anchor by that move (2 px to 4 px), not by the 4 px since the press.
let acts = pen.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 104.0, 100.0).with_mods(space));
let Some(Action::Preview(c, v)) = acts.first() else { panic!("not a preview: {acts:?}") };
assert_eq!(c, "path.create");
assert_eq!(v["anchors"][0]["x"], json!(102.0), "moved by the delta");
assert_eq!(v["anchors"][0]["y"], json!(100.0));
}
#[test]
fn space_moves_the_anchor_being_placed() {
let (mut d, _) = doc_with_rect();
+28 -1
View File
@@ -484,7 +484,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
{
crate::place::paint_cursor(app, ui.ctx(), &painter, hp);
}
// Caps Lock gives precise (crosshair) cursors, like Illustrator; env opt-out for system cursors.
// The glyph cursors are drawn here; General › Use Precise Cursors (the session turns the drawing tools' pointers
// into crosshairs, `Cursor::precise`) needs no handling. VECTORCRAFT_SYSTEM_CURSORS opts out of the glyphs.
let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none();
// User Interface › Scale Cursor Proportional to UI: the glyphs grow with UI Scaling;
// off, they keep their size.
@@ -2209,6 +2210,32 @@ mod tests {
use vectorcraft_engine::Session;
use vectorcraft_geom::Shape as _;
/// With VECTORCRAFT_SYSTEM_CURSORS set the glyphs aren't drawn and the system's cursors stand in: those of
/// the Pen and the other drawing tools are the system crosshair already, so Use Precise Cursors makes no
/// difference to them there.
#[test]
fn system_cursors_show_the_same_crosshair_with_precise_cursors_on_or_off() {
for c in [
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenContinue,
Cursor::PenJoin,
Cursor::PenConvert,
Cursor::Eyedropper,
Cursor::Slice,
Cursor::Blend,
Cursor::BlendObject,
Cursor::BlendAnchor,
Cursor::ShapeBuilder,
Cursor::ShapeBuilderErase,
] {
assert_eq!(cursor_icon(c), egui::CursorIcon::Crosshair, "{c:?}");
assert_eq!(cursor_icon(c), cursor_icon(c.precise()), "{c:?}");
}
}
/// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one
/// reads x where the canvas has it.
#[test]
+1
View File
@@ -494,6 +494,7 @@
Three Point Perspective Tříbodová perspektiva
Save Grid as Preset… Uložit mřížku jako přednastavení…
Snap to Pixel Přichytávat k obrazovým bodům
Precise Cursors Přesné kurzory
Snap to Point Přichytávat k bodům
Snap to Glyph Přichytávat ke glyfům
New View… Nové zobrazení…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2724,6 +2724,7 @@ effect Points Punkte
Snap to Glyph An Glyphe ausrichten
Snap to Grid Am Raster ausrichten
Snap to Pixel An Pixel ausrichten
Precise Cursors Präzise Cursor
Snap to Point An Punkt ausrichten
Snapping Tolerance Einrasttoleranz
Social Square Post 1080×1080 Quadratischer Social-Media-Beitrag 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2774,6 +2774,7 @@
Snap to Glyph Ajustar a glifo
Snap to Grid Ajustar a cuadrícula
Snap to Pixel Ajustar a píxel
Precise Cursors Cursores precisos
Snap to Point Ajustar a punto
Snapping Tolerance Tolerancia de ajuste
Social Square Post 1080×1080 Publicación cuadrada para redes 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2771,6 +2771,7 @@
Snap to Glyph Magnétisme du glyphe
Snap to Grid Magnétisme de la grille
Snap to Pixel Magnétisme du pixel
Precise Cursors Pointeurs précis
Snap to Point Magnétisme du point
Snapping Tolerance Tolérance du magnétisme
Social Square Post 1080×1080 Publication carrée pour réseaux sociaux 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3358,6 +3358,7 @@
Snap to Glyph Illesztés glifához
Snap to Grid Illesztés a rácshoz
Snap to Pixel Illesztés pixelhez
Precise Cursors Pontos kurzorok
Snap to Point Illesztés pontra
Snapping Tolerance Illesztési tűrés
Social Közösségi média
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2774,6 +2774,7 @@
Snap to Glyph Aggancia a glifo
Snap to Grid Aggancia alla griglia
Snap to Pixel Aggancia ai pixel
Precise Cursors Cursori di precisione
Snap to Point Aggancia al punto
Snapping Tolerance Tolleranza di aggancio
Social Square Post 1080×1080 Post quadrato per social 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -514,6 +514,7 @@
Three Point Perspective 3 点遠近法
Save Grid as Preset… グリッドをプリセットとして保存…
Snap to Pixel ピクセルにスナップ
Precise Cursors 精密カーソル
Snap to Point ポイントにスナップ
Snap to Glyph グリフにスナップ
New View… 新規表示…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -512,6 +512,7 @@
Three Point Perspective 3점 투시
Save Grid as Preset… 격자를 프리셋으로 저장…
Snap to Pixel 픽셀에 스냅
Precise Cursors 정밀 커서
Snap to Point 포인트에 스냅
Snap to Glyph 글리프에 스냅
New View… 새 보기…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -489,6 +489,7 @@
Three Point Perspective Perspektywa trzypunktowa
Save Grid as Preset… Zapisz siatkę jako ustawienie…
Snap to Pixel Przyciągaj do pikseli
Precise Cursors Precyzyjne kursory
Snap to Point Przyciągaj do punktów
Snap to Glyph Przyciągaj do glifów
New View… Nowy widok…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2306,6 +2306,7 @@
Snap to Glyph Ajustar ao Glifo
Snap to Grid Ajustar à Grade
Snap to Pixel Ajustar ao Pixel
Precise Cursors Cursores Precisos
Snap to Point Ajustar ao Ponto
Snapping Tolerance Tolerância de Ajuste
Soft Light Luz Suave
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2770,6 +2770,7 @@
Snap to Glyph Привязать к глифу
Snap to Grid Привязать к сетке
Snap to Pixel Привязать к пикселю
Precise Cursors Точные курсоры
Snap to Point Привязать к точке
Snapping Tolerance Допуск привязки
Social Square Post 1080×1080 Квадратный пост для соцсетей 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2767,6 +2767,7 @@ effect Points Точки
Snap to Glyph Прив’язати до гліфу
Snap to Grid Прив’язати до сітки
Snap to Pixel Прив’язати до пікселя
Precise Cursors Точні курсори
Snap to Point Прив’язати до точки
Snapping Tolerance Допуск прив’язки
Social Square Post 1080×1080 Квадратний пост для соцмереж 1080×1080
Can't render this file because it contains an unexpected character in line 713 and column 40.
+1
View File
@@ -3075,6 +3075,7 @@
Snap to Glyph Hút vào glyph
Snap to Grid Hút vào lưới
Snap to Pixel Hút vào pixel
Precise Cursors Con trỏ chính xác
Snap to Point Hút vào điểm
Snapping Tolerance Dung sai hút
Social Square Post 1080×1080 Bài đăng vuông mạng xã hội 1080×1080
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
3075
3076
3077
3078
3079
3080
3081
+1
View File
@@ -2661,6 +2661,7 @@
Snap to Glyph 靠齐字符
Snap to Grid 对齐网格
Snap to Pixel 对齐像素
Precise Cursors 精确光标
Snap to Point 对齐点
Snapping Tolerance 吸附容差
Social Square Post 1080×1080 社交方形帖子 1080×1080
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2752,6 +2752,7 @@
Snap to Glyph 靠齊字符
Snap to Grid 靠齊格點
Snap to Pixel 靠齊像素
Precise Cursors 精確游標
Snap to Point 靠齊控制點
Snapping Tolerance 靠齊容許度
Social Square Post 1080×1080 社群正方形貼文 1080×1080
Can't render this file because it contains an unexpected character in line 6 and column 47.
+64
View File
@@ -186,6 +186,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.trimView", "Trim View", "", "{} toggle: hide everything outside the artboards"),
("view.cornerWidget", "Hide Corner Widget", "", "{} toggle the live corner widgets"),
("view.snapToPixel", "Snap to Pixel", "", "{} toggle: drawing and moving land on whole pixels"),
(
"view.preciseCursors",
"Precise Cursors",
"",
"{} toggle General › Use Precise Cursors: the drawing tools' pointers become a crosshair → true when on",
),
("view.textThreads", "Hide Text Threads", "Cmd+Shift+Y", "{} toggle the thread lines between threaded text frames"),
("view.gradientAnnotator", "Hide Gradient Annotator", "Cmd+Alt+G", "{} toggle the Gradient tool's annotator"),
("type.hiddenCharacters", "Show Hidden Characters", "Cmd+Alt+I", "{} toggle markers for spaces, paragraph ends and story ends"),
@@ -1053,6 +1059,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
None => Err("no such saved selection".into()),
},
"view.snapToPixel" => flag(&mut app.ui.view.snap_to_pixel),
// The preference itself (General › Use Precise Cursors), so the command, the dialog and the saved
// preferences share one value.
"view.preciseCursors" => {
let on = !app.session.prefs.use_precise_cursors;
app.run("prefs.set", json!({"key": "usePreciseCursors", "value": on})).map(|_| {
crate::prefs_dialog::snapshot(app);
json!(on)
})
}
"view.textThreads" => flag(&mut app.ui.view.text_threads),
"type.hiddenCharacters" => flag(&mut app.ui.view.hidden_chars),
"type.bold" => crate::panels::character::toggle_face(app, Face::Bold),
@@ -3993,6 +4008,55 @@ mod tests {
assert_eq!(dynamic_label(&app, "view.cornerWidget", ""), "Show Corner Widget");
}
/// View › Precise Cursors has no menu item or default key, but the command (assignable in Keyboard Shortcuts)
/// writes General › Use Precise Cursors, the value the dialog and the saved preferences read.
#[test]
fn precise_cursors_command_toggles_the_preference() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
assert!(!app.session.prefs.use_precise_cursors);
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(true));
assert!(app.session.prefs.use_precise_cursors);
assert_eq!(app.ui.engine_prefs["usePreciseCursors"], json!(true), "saved with the UI state");
assert!(UI_COMMANDS.iter().any(|c| c.0 == "view.preciseCursors" && c.2.is_empty()), "assignable, no default key");
assert_eq!(app.run("view.preciseCursors", json!({})).unwrap(), json!(false));
assert!(!app.session.prefs.use_precise_cursors);
}
/// The command is listed in Edit › Keyboard Shortcuts without a key and takes one the user types; with the
/// key it is the command that runs.
#[test]
fn precise_cursors_command_is_listed_and_assignable_in_the_shortcut_editor() {
use crate::shortcut_editor as ed;
let e = ed::entry("view.preciseCursors").expect("listed in the editor");
assert_eq!((e.label.as_str(), e.group.as_str(), e.is_tool), ("Precise Cursors", "View", false));
assert_eq!(ed::default_of("view.preciseCursors"), None, "no default key");
let mut overrides = std::collections::BTreeMap::new();
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None);
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+U"), false), Ok(vec![]));
let chord = ed::effective_in(&overrides, "view.preciseCursors").expect("assigned");
assert_eq!(ed::normalize(&chord), ed::normalize("Cmd+Alt+Shift+U"));
// A key another command holds is refused, so the new command can't take it over silently.
assert!(ed::assign(&mut overrides, "view.preciseCursors", Some("Cmd+Alt+Shift+P"), false).is_err());
assert_eq!(ed::assign(&mut overrides, "view.preciseCursors", None, false), Ok(vec![]));
assert_eq!(ed::effective_in(&overrides, "view.preciseCursors"), None, "cleared again");
}
/// The command is a preference, not an edit: no undo step, no unsaved changes, with a document open.
#[test]
fn precise_cursors_command_is_not_an_edit() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let steps = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
(st.history.undo.len(), st.history.redo.len(), st.is_dirty())
};
let before = steps(&app);
assert!(!before.2);
app.run("view.preciseCursors", json!({})).unwrap();
assert!(app.session.prefs.use_precise_cursors);
assert_eq!(steps(&app), before);
}
/// Selection & Anchor Display › Zoom to Selection (#394): Zoom In and Zoom Out bring the
/// selection to the middle; off, they keep the view's centre.
#[test]