Add a Precise Cursors command and fix the misleading Caps Lock comment

General > Use Precise Cursors already worked and is saved with the other
preferences. This adds a `view.preciseCursors` command that toggles that same
preference, so it can be given a shortcut in Edit > Keyboard Shortcuts. It has
no default shortcut and no View menu item.

The comment in canvas.rs claimed Caps Lock gave precise cursors; nothing reads
Caps Lock (egui has no state for it), so it now describes what the code does.

Tests: for the tools whose own pointer the preference replaces (Pen, the anchor
tools, Curvature, Mesh, Eyedropper, Slice, Blend, Shape Builder) the pointer is
their own when off and a crosshair when on; every state of the Pen's pointer
turns into a crosshair; the preference round-trips through
to_json/apply_prefs; the command toggles it, updates the saved UI state, is
listed in the shortcut editor without a key and can be assigned one; switching
it creates no undo step and leaves the document saved; with system cursors the
glyph fallback is the system crosshair either way. The new label is translated
in the languages that claim complete menus.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
ham9zahandClaude Sonnet 5.5 authored and bflatastic committed 2026-10-14 03:55:41 +07:00
1 parent 3ea4802a53
commit 66a0f457e9
18 files changed
+235 -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 {
+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]