mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
When entering text edit mode via double-click from a selection tool (Selection or Direct Selection), pressing Escape now returns to the specific tool used before double-clicking (rather than always falling back to Selection). Additionally, when editing text while in isolation mode (such as when drilling into nested groups with the Selection tool), pressing Escape exits text editing while keeping isolation mode active; a second Escape press is required to exit isolation mode. - Record the previous tool in TypeTool on double-click from canvas. - Return to prev_tool on ToolKey::Escape, consuming the key in shortcuts when claimed. - Add unit test in vectorcraft-tools verifying Escape returns to prev_tool. - Add integration tests in vectorcraft-ui-egui verifying Escape returns to Direct Selection and that isolation mode persists until a second Escape. - Update docs/target-app-parity.md and ROADMAP.md.
403 lines
18 KiB
Rust
403 lines
18 KiB
Rust
//! Type tool state-machine tests (the engine side is covered in vectorcraft-engine's
|
|
//! `tests_textedit`).
|
|
|
|
use super::*;
|
|
use crate::testutil::{cx, paint};
|
|
use vectorcraft_doc::{CharStyle, Document, Node, Selection};
|
|
|
|
fn doc_with_text(text: &str) -> (Document, NodeId) {
|
|
let mut d = Document::new(500.0, 500.0);
|
|
let l = d.layers[0].id;
|
|
let id = d.alloc_id();
|
|
let t = TextObject::point(Point::new(100.0, 100.0), text, CharStyle { size: 20.0, ..Default::default() });
|
|
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
|
(d, id)
|
|
}
|
|
|
|
fn editing(text: &str) -> (Document, NodeId, TypeTool) {
|
|
let (d, id) = doc_with_text(text);
|
|
let mut tool = TypeTool::new("type");
|
|
tool.start_editing(id, 0);
|
|
(d, id, tool)
|
|
}
|
|
|
|
fn key(tool: &mut TypeTool, d: &Document, k: ToolKey, mods: Mods) -> Vec<Action> {
|
|
let (sel, p) = (Selection::default(), paint());
|
|
tool.key(&cx(d, &sel, &p), k, mods)
|
|
}
|
|
|
|
const SHIFT: Mods = Mods { shift: true, alt: false, cmd: false, ctrl: false, space: false };
|
|
const CMD: Mods = Mods { shift: false, alt: false, cmd: true, ctrl: false, space: false };
|
|
|
|
/// Apply the tool's previews like the engine would (on top of the session snapshot).
|
|
fn apply(d: &mut Document, snapshot: &Document, acts: &[Action]) {
|
|
for a in acts {
|
|
if let Action::Preview(cmd, p) = a {
|
|
assert_eq!(cmd, "text.editRange");
|
|
*d = snapshot.clone();
|
|
let id = NodeId(p["id"].as_u64().unwrap());
|
|
let runs: Vec<TextRun> = serde_json::from_value(p["runs"].clone()).unwrap();
|
|
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
|
edit::replace_range_styled(&mut t.runs, p["start"].as_u64().unwrap() as usize, p["end"].as_u64().unwrap() as usize, &runs);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typing_is_one_coalesced_preview() {
|
|
let (mut d, id, mut tool) = editing("Hello");
|
|
let snap = d.clone();
|
|
let (sel, p) = (Selection::default(), paint());
|
|
tool.caret = 5;
|
|
tool.anchor = 5;
|
|
let a1 = tool.text_input(&cx(&d, &sel, &p), " w");
|
|
assert_eq!(a1[0], Action::Begin("Typing".into()));
|
|
apply(&mut d, &snap, &a1);
|
|
let a2 = tool.text_input(&cx(&d, &sel, &p), "orld");
|
|
apply(&mut d, &snap, &a2);
|
|
let NodeKind::Text(t) = &d.node(id).unwrap().kind else { panic!() };
|
|
assert_eq!(t.plain_text(), "Hello world");
|
|
assert!(!a2.iter().any(|a| matches!(a, Action::Begin(_))), "same session");
|
|
let Action::Preview(cmd, params) = a2.last().unwrap() else { panic!("{a2:?}") };
|
|
assert_eq!(cmd, "text.editRange");
|
|
assert_eq!(params["id"], json!(id.0));
|
|
assert_eq!((params["start"].as_u64(), params["end"].as_u64()), (Some(5), Some(5)));
|
|
let runs: Vec<TextRun> = serde_json::from_value(params["runs"].clone()).unwrap();
|
|
assert_eq!(runs[0].text, " world");
|
|
assert_eq!(tool.caret, 11);
|
|
// Moving the caret ends the session.
|
|
let a3 = key(&mut tool, &d, ToolKey::Left, Mods::default());
|
|
assert_eq!(a3, vec![Action::Commit]);
|
|
assert_eq!(tool.caret, 10);
|
|
// A document change behind the tool's back (undo) starts a fresh session.
|
|
let a4 = tool.text_input(&cx(&snap, &sel, &p), "!");
|
|
assert!(a4.contains(&Action::Begin("Typing".into())));
|
|
}
|
|
|
|
#[test]
|
|
fn shift_arrows_select_and_typing_replaces() {
|
|
let (d, _, mut tool) = editing("Hello world");
|
|
key(&mut tool, &d, ToolKey::End, Mods::default());
|
|
assert_eq!(tool.caret, 11);
|
|
key(&mut tool, &d, ToolKey::Left, Mods { alt: true, ..SHIFT });
|
|
assert_eq!(tool.sel(), (6, 11));
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let acts = tool.text_input(&cx(&d, &sel, &p), "there");
|
|
let Action::Preview(_, params) = acts.last().unwrap() else { panic!() };
|
|
assert_eq!((params["start"].as_u64(), params["end"].as_u64()), (Some(6), Some(11)));
|
|
assert_eq!(tool.caret, 11);
|
|
assert_eq!(tool.sel(), (11, 11));
|
|
}
|
|
|
|
#[test]
|
|
fn word_and_line_navigation_keys() {
|
|
let (d, _, mut tool) = editing("One two three");
|
|
key(&mut tool, &d, ToolKey::Right, CMD);
|
|
assert_eq!(tool.caret, 3);
|
|
key(&mut tool, &d, ToolKey::Right, CMD);
|
|
assert_eq!(tool.caret, 7);
|
|
key(&mut tool, &d, ToolKey::Left, CMD);
|
|
assert_eq!(tool.caret, 4);
|
|
key(&mut tool, &d, ToolKey::End, SHIFT);
|
|
assert_eq!(tool.sel(), (4, 13));
|
|
// Left collapses a selection to its start.
|
|
key(&mut tool, &d, ToolKey::Left, Mods::default());
|
|
assert_eq!(tool.sel(), (4, 4));
|
|
key(&mut tool, &d, ToolKey::Home, Mods::default());
|
|
assert_eq!(tool.caret, 0);
|
|
key(&mut tool, &d, ToolKey::End, CMD);
|
|
assert_eq!(tool.caret, 13);
|
|
}
|
|
|
|
#[test]
|
|
fn up_down_move_between_lines() {
|
|
let (d, _, mut tool) = editing("First line\nSecond line");
|
|
tool.caret = 3;
|
|
tool.anchor = 3;
|
|
key(&mut tool, &d, ToolKey::Down, Mods::default());
|
|
assert!(tool.caret > 11 && tool.caret < 16, "{}", tool.caret);
|
|
key(&mut tool, &d, ToolKey::Up, Mods::default());
|
|
assert_eq!(tool.caret, 3, "goal column kept");
|
|
key(&mut tool, &d, ToolKey::Down, SHIFT);
|
|
key(&mut tool, &d, ToolKey::Down, SHIFT);
|
|
assert_eq!(tool.sel(), (3, 22));
|
|
}
|
|
|
|
#[test]
|
|
fn backspace_deletes_selection_or_word() {
|
|
let (d, _, mut tool) = editing("Hello big world");
|
|
tool.caret = 9;
|
|
tool.anchor = 9;
|
|
let acts = key(&mut tool, &d, ToolKey::Backspace, Mods { alt: true, ..Default::default() });
|
|
let Action::Preview(_, params) = acts.last().unwrap() else { panic!() };
|
|
assert_eq!((params["start"].as_u64(), params["end"].as_u64()), (Some(6), Some(9)));
|
|
assert_eq!(tool.caret, 6);
|
|
}
|
|
|
|
#[test]
|
|
fn double_and_triple_click_select_word_and_paragraph() {
|
|
let (d, id, mut tool) = editing("Alpha beta\ngamma");
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let t = TypeTool::text(&cx(&d, &sel, &p), id).unwrap().clone();
|
|
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
|
let (top, bot) = vectorcraft_text::caret_position(&lay, 7);
|
|
let at = t.xf * top.midpoint(bot);
|
|
let c = cx(&d, &sel, &p);
|
|
for k in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] {
|
|
tool.pointer(&c, &PointerEvent::new(k, at.x, at.y));
|
|
}
|
|
assert_eq!(tool.sel(), (6, 10), "word");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x, at.y));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, at.x, at.y));
|
|
assert_eq!(tool.sel(), (0, 10), "paragraph");
|
|
}
|
|
|
|
#[test]
|
|
fn drag_selects_and_overlay_highlights() {
|
|
let (d, id, mut tool) = editing("Select me please");
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let c = cx(&d, &sel, &p);
|
|
let t = TypeTool::text(&c, id).unwrap().clone();
|
|
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
|
let pt = |b: usize| {
|
|
let (a, bb) = vectorcraft_text::caret_position(&lay, b);
|
|
t.xf * a.midpoint(bb)
|
|
};
|
|
let (a, b) = (pt(7), pt(9));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, a.x + 0.1, a.y));
|
|
assert!(tool.busy());
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, b.x + 0.1, b.y));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, b.x + 0.1, b.y));
|
|
assert_eq!(tool.sel(), (7, 9));
|
|
let o = tool.overlays(&c);
|
|
assert!(o.iter().any(|o| matches!(o, Overlay::Highlight { .. })));
|
|
let opts = tool.options();
|
|
assert_eq!((opts["start"].as_u64(), opts["end"].as_u64()), (Some(7), Some(9)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_drag_across_type_not_being_edited_selects_its_text() {
|
|
let (d, id) = doc_with_text("Select me please");
|
|
let mut tool = TypeTool::new("type");
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let c = cx(&d, &sel, &p);
|
|
let t = TypeTool::text(&c, id).unwrap().clone();
|
|
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
|
let pt = |b: usize| {
|
|
let (a, bb) = vectorcraft_text::caret_position(&lay, b);
|
|
t.xf * a.midpoint(bb)
|
|
};
|
|
let (a, b) = (pt(7), pt(9));
|
|
// The press starts editing the type and the drag selects, in one gesture.
|
|
let out = tool.pointer(&c, &PointerEvent::new(PointerKind::Down, a.x + 0.1, a.y));
|
|
assert!(out.contains(&Action::Exec("select.set".into(), json!({"ids": [id.0]}))));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, b.x + 0.1, b.y));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, b.x + 0.1, b.y));
|
|
assert_eq!(tool.editing, Some(id));
|
|
assert_eq!(tool.sel(), (7, 9));
|
|
// A plain click still just places the caret.
|
|
let mut tool = TypeTool::new("type");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, a.x + 0.1, a.y));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, a.x + 0.1, a.y));
|
|
assert_eq!((tool.editing, tool.sel()), (Some(id), (7, 7)));
|
|
}
|
|
|
|
#[test]
|
|
fn select_all_and_styled_paste() {
|
|
let (d, _, mut tool) = editing("abc");
|
|
tool.set_option("selectAll", &json!(true));
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let c = cx(&d, &sel, &p);
|
|
tool.key(&c, ToolKey::Right, SHIFT);
|
|
assert_eq!(tool.sel(), (0, 3));
|
|
let big = TextRun { text: "XY".into(), style: CharStyle { size: 40.0, ..Default::default() }, inline: None };
|
|
tool.set_option("copy", &serde_json::to_value(vec![big.clone()]).unwrap());
|
|
let acts = tool.text_input(&c, "XY");
|
|
let Action::Preview(_, params) = acts.last().unwrap() else { panic!() };
|
|
let runs: Vec<TextRun> = serde_json::from_value(params["runs"].clone()).unwrap();
|
|
assert_eq!(runs, vec![big]);
|
|
}
|
|
|
|
#[test]
|
|
fn common_affixes_are_char_safe() {
|
|
assert_eq!(common_affixes("hello", "help", usize::MAX), (3, 0));
|
|
assert_eq!(common_affixes("aXb", "ab", usize::MAX), (1, 1));
|
|
assert_eq!(common_affixes("aaa", "aaaa", usize::MAX), (3, 0));
|
|
assert_eq!(common_affixes("é1", "è1", usize::MAX), (0, 1));
|
|
// Return at the end of a paragraph: the span starts where the edit did (before the old break).
|
|
assert_eq!(common_affixes("abc\ndef", "abc\n\ndef", 3), (3, 4));
|
|
assert_eq!(common_affixes("abc\ndef", "abc\n\ndef", 4), (4, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn area_and_path_tools_convert_clicked_paths() {
|
|
let (d, rect) = crate::testutil::doc_with_rect();
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let c = cx(&d, &sel, &p);
|
|
let mut tool = TypeTool::new("areaType");
|
|
assert_eq!(tool.id(), "areaType");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 150.0));
|
|
let acts = tool.pointer(&c, &PointerEvent::new(PointerKind::Up, 100.0, 150.0));
|
|
assert!(
|
|
acts.iter().any(|a| matches!(a, Action::Exec(cmd, p) if cmd == "text.createInPath" && p["path"] == json!(rect.0) && p["mode"] == "area")),
|
|
"{acts:?}"
|
|
);
|
|
let mut tool = TypeTool::new("typeOnPath");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 100.0));
|
|
let acts = tool.pointer(&c, &PointerEvent::new(PointerKind::Up, 150.0, 100.0));
|
|
assert!(acts.iter().any(|a| matches!(a, Action::Exec(cmd, p) if cmd == "text.createInPath" && p["mode"] == "onPath")), "{acts:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_tools_preserve_their_ids_and_create_vertical_text() {
|
|
for id in ["verticalType", "verticalAreaType", "verticalTypeOnPath"] {
|
|
let tool = TypeTool::new(id);
|
|
assert_eq!(tool.id(), id);
|
|
assert!(tool.vertical);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ime_ranges_convert_characters_to_bytes() {
|
|
assert_eq!(char_range_to_bytes("ががく", 1..3), Some(3..9));
|
|
assert_eq!(char_range_to_bytes("ががく", 3..3), Some(9..9));
|
|
assert_eq!(char_range_to_bytes("a雅b", 1..2), Some(1..4));
|
|
assert_eq!(char_range_to_bytes("ががく", 2..4), None, "past the end");
|
|
#[allow(clippy::reversed_empty_ranges)]
|
|
let backwards = 2..1;
|
|
assert_eq!(char_range_to_bytes("ががく", backwards), None, "backwards");
|
|
}
|
|
|
|
#[test]
|
|
fn marked_text_is_underlined_with_the_converting_clause_thick() {
|
|
let (mut d, id, mut tool) = editing("曲");
|
|
let snap = d.clone();
|
|
let (sel, p) = (Selection::default(), paint());
|
|
tool.caret = 3;
|
|
tool.anchor = 3;
|
|
let acts = tool.ime_preedit(&cx(&d, &sel, &p), "雅楽演奏", Some(2..4));
|
|
apply(&mut d, &snap, &acts);
|
|
let NodeKind::Text(t) = &d.node(id).unwrap().kind else { panic!() };
|
|
assert_eq!(t.plain_text(), "曲雅楽演奏");
|
|
assert_eq!(tool.preedit, Some(Preedit { range: 3..15, active: Some(6..12) }));
|
|
let widths: Vec<f32> = tool
|
|
.overlays(&cx(&d, &sel, &p))
|
|
.iter()
|
|
.filter_map(|o| match o {
|
|
Overlay::Path { width, .. } => Some(*width),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(widths, vec![1.0, 2.5], "a thin underline, then the thick one under 演奏");
|
|
// The candidate window follows the converting clause.
|
|
let (top, _) = tool.ime_caret(&cx(&d, &sel, &p)).unwrap();
|
|
let lay = vectorcraft_text::layout(FontDb::global(), t);
|
|
let (at, _) = vectorcraft_text::caret_position(&lay, 9);
|
|
assert!((top - t.xf * at).hypot() < 1e-6);
|
|
// Keys wait for the IME; a commit replaces the marked text.
|
|
assert!(key(&mut tool, &d, ToolKey::Backspace, Mods::default()).is_empty());
|
|
let acts = tool.text_input(&cx(&d, &sel, &p), "雅楽演奏会");
|
|
apply(&mut d, &snap, &acts);
|
|
let NodeKind::Text(t) = &d.node(id).unwrap().kind else { panic!() };
|
|
assert_eq!(t.plain_text(), "曲雅楽演奏会");
|
|
assert!(!tool.composing());
|
|
}
|
|
|
|
#[test]
|
|
fn arrows_follow_the_columns_of_vertical_type() {
|
|
let (mut d, id) = doc_with_text("§§§\n§§§");
|
|
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
|
t.vertical = true;
|
|
}
|
|
let mut tool = TypeTool::new("type");
|
|
tool.start_editing(id, 0);
|
|
let s = "§".len();
|
|
key(&mut tool, &d, ToolKey::Down, Mods::default());
|
|
assert_eq!(tool.caret, s, "↓ goes down the column to the next character");
|
|
key(&mut tool, &d, ToolKey::Left, Mods::default());
|
|
assert_eq!(tool.caret, "§§§\n§".len(), "← goes on to the next column, keeping the place in it");
|
|
key(&mut tool, &d, ToolKey::Right, Mods::default());
|
|
assert_eq!(tool.caret, s, "→ comes back");
|
|
key(&mut tool, &d, ToolKey::Up, Mods::default());
|
|
assert_eq!(tool.caret, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn hebrew_typing_and_visual_arrows_keep_logical_text() {
|
|
let (mut d, id, mut tool) = editing("");
|
|
let snap = d.clone();
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let actions = tool.text_input(&cx(&d, &sel, &p), "שלום");
|
|
apply(&mut d, &snap, &actions);
|
|
assert_eq!(tool.caret, "שלום".len());
|
|
key(&mut tool, &d, ToolKey::Right, Mods::default());
|
|
assert_eq!(tool.caret, 6, "Right moves toward the logical start in Hebrew");
|
|
key(&mut tool, &d, ToolKey::Left, Mods::default());
|
|
assert_eq!(tool.caret, 8);
|
|
let NodeKind::Text(t) = &d.node(id).unwrap().kind else { panic!() };
|
|
assert_eq!(t.plain_text(), "שלום");
|
|
}
|
|
|
|
/// Where new type goes snaps to Smart Guides (#506): a click beside a rect lines up with its
|
|
/// centre, and hovering shows it first; an area's corners land on its anchors.
|
|
#[test]
|
|
fn new_type_snaps_to_smart_guides() {
|
|
let (d, _) = crate::testutil::doc_with_rect();
|
|
let (sel, p) = (Selection::default(), paint());
|
|
let c = cx(&d, &sel, &p);
|
|
let mut tool = TypeTool::new("type");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Move, 151.0, 330.0));
|
|
assert!(tool.overlays(&c).iter().any(|o| matches!(o, Overlay::Label { text, p, .. } if text == "align" && *p == Point::new(150.0, 330.0))));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, 151.0, 330.0));
|
|
let acts = tool.pointer(&c, &PointerEvent::new(PointerKind::Up, 151.0, 330.0));
|
|
assert!(acts.iter().any(|a| matches!(a, Action::Exec(cmd, v) if cmd == "text.create" && v["x"] == 150.0 && v["y"] == 330.0)), "{acts:?}");
|
|
let mut tool = TypeTool::new("type");
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, 102.0, 98.0));
|
|
tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, 199.0, 202.0));
|
|
let acts = tool.pointer(&c, &PointerEvent::new(PointerKind::Up, 199.0, 202.0));
|
|
let area = json!({"width": 100.0, "height": 100.0});
|
|
assert!(acts.iter().any(|a| matches!(a, Action::Exec(cmd, v) if cmd == "text.create" && v["x"] == 100.0 && v["area"] == area)), "{acts:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn layout_cache_follows_fonts_added_later() {
|
|
// Other tests can load system fonts; this assertion needs its own process-wide database.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
if std::env::var("VECTORCRAFT_FONT_CACHE_TEST_CHILD").as_deref() != Ok("1") {
|
|
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
.args(["--exact", "text::tests::layout_cache_follows_fonts_added_later", "--nocapture", "--test-threads=1"])
|
|
.env("VECTORCRAFT_FONT_CACHE_TEST_CHILD", "1")
|
|
.output()
|
|
.unwrap();
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
|
|
"isolated font cache test failed:\n{stdout}\n{}",
|
|
String::from_utf8_lossy(&output.stderr),
|
|
);
|
|
return;
|
|
}
|
|
// The web build adds fonts after startup: a caret layout cached before one arrived is stale.
|
|
let (d, id, tool) = editing("abc");
|
|
let NodeKind::Text(t) = &d.node(id).unwrap().kind else { panic!("not text") };
|
|
let before = tool.layout(t);
|
|
assert!(Arc::ptr_eq(&before, &tool.layout(t)), "cached while the fonts don't change");
|
|
let font = vectorcraft_text::test_fonts::variable_font().unwrap();
|
|
assert!(FontDb::global().add_font(font) > 0, "the test font is new to this process");
|
|
assert!(!Arc::ptr_eq(&before, &tool.layout(t)), "a font added after the layout must refresh it");
|
|
}
|
|
|
|
#[test]
|
|
fn escape_returns_to_previous_tool_when_set() {
|
|
let (d, _id, mut tool) = editing("Hello");
|
|
tool.set_option("prevTool", &json!("directSelection"));
|
|
let acts = key(&mut tool, &d, ToolKey::Escape, Mods::default());
|
|
assert_eq!(acts.last(), Some(&Action::SwitchTool("directSelection".into())));
|
|
|
|
// Without prevTool set, Escape defaults to Selection tool.
|
|
let (d2, _id2, mut tool2) = editing("World");
|
|
let acts2 = key(&mut tool2, &d2, ToolKey::Escape, Mods::default());
|
|
assert_eq!(acts2.last(), Some(&Action::SwitchTool("selection".into())));
|
|
}
|