mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Merge pull request #4745 from dotCooCoo/feat/escape-leaves-full-screen
View: Escape in either Full Screen Mode returns to Normal Screen Mode
This commit is contained in:
@@ -500,6 +500,9 @@ impl VectorcraftApp {
|
||||
titlebar::resize_zones(ui);
|
||||
}
|
||||
self.ui_fonts.frame(&ctx);
|
||||
if self.ui.dialog.is_none() && !self.ui.palette_open {
|
||||
shortcuts::remember_text_focus(&ctx);
|
||||
}
|
||||
scrub::end_frame(self, &ctx);
|
||||
native_menu::sync(self, &ctx);
|
||||
// Observe once, after all of this pass's input, dialogs, floating panels, palette and
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Arc, Ordering, VectorcraftApp, graphics, native_menu, shortcuts, take_frame_once};
|
||||
use super::{Arc, Ordering, VectorcraftApp, graphics, native_menu, shortcuts, take_frame_once, tools_float};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use super::{fileio, place};
|
||||
|
||||
@@ -15,6 +15,7 @@ impl VectorcraftApp {
|
||||
self.write_formats_before_text_input(ctx);
|
||||
native_menu::run(self, ctx);
|
||||
if self.fonts_ready {
|
||||
tools_float::cancel_move_on_escape(self, ctx);
|
||||
shortcuts::handle(self, ctx);
|
||||
}
|
||||
// A Copy taken in this UI phase (an egui copy event, or a command it runs) is published in
|
||||
|
||||
@@ -164,6 +164,11 @@ enum Drag {
|
||||
Present,
|
||||
}
|
||||
|
||||
/// Whether the canvas is holding a pointer drag across frames.
|
||||
pub(crate) fn dragging(ctx: &egui::Context) -> bool {
|
||||
ctx.data(|d| d.get_temp::<Drag>(drag_id()).is_some())
|
||||
}
|
||||
|
||||
/// Paint the document canvas, complete or cached artwork, navigation and tool overlays.
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
|
||||
@@ -24,6 +24,22 @@ use typing::is_shortcut_chord;
|
||||
pub(crate) use typing::keep_marked_text;
|
||||
use typing::{drop_typing_presses, type_text};
|
||||
|
||||
fn text_focus_frame_id() -> egui::Id {
|
||||
egui::Id::unique("shortcut-text-focus-frame")
|
||||
}
|
||||
|
||||
pub(crate) fn remember_text_focus(ctx: &egui::Context) {
|
||||
if ctx.egui_wants_keyboard_input() {
|
||||
let frame = ctx.cumulative_frame_nr_for(ctx.viewport_id());
|
||||
ctx.data_mut(|d| d.insert_temp(text_focus_frame_id(), frame));
|
||||
}
|
||||
}
|
||||
|
||||
fn text_focused_in_previous_frame(ctx: &egui::Context) -> bool {
|
||||
let previous = ctx.cumulative_frame_nr_for(ctx.viewport_id()).checked_sub(1);
|
||||
ctx.data(|d| d.get_temp::<u64>(text_focus_frame_id())) == previous
|
||||
}
|
||||
|
||||
/// Parse "Cmd+Shift+]" into an egui shortcut. `Cmd` is Command on macOS and Ctrl elsewhere.
|
||||
/// `+` and `=` name one chord key ([`Key::Equals`]): `+` is Shift+`=` on many layouts, so a
|
||||
/// `Cmd+=` chord also answers `+` however it is typed.
|
||||
@@ -189,9 +205,11 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
presentation_keys(app, ctx, escape_pair);
|
||||
return;
|
||||
}
|
||||
let escape_held_by_drag =
|
||||
crate::canvas::dragging(ctx) || crate::canvas::ruler_origin_dragging(ctx) || crate::tools_float::escape_cancelled_move(ctx);
|
||||
let typing = ctx.egui_wants_keyboard_input();
|
||||
// Escape closes Edit Toolbar's drawer first (egui drops a drag from it on Escape already).
|
||||
if app.ui.tools_drawer && !typing && ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)) {
|
||||
if app.ui.tools_drawer && !typing && !escape_held_by_drag && ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)) {
|
||||
app.ui.tools_drawer = false;
|
||||
}
|
||||
let view = app.view_info();
|
||||
@@ -232,7 +250,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ctx.input_mut(|i| i.consume_key(Modifiers::NONE, k));
|
||||
ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, k));
|
||||
}
|
||||
if k == Key::Escape && !busy && !claimed {
|
||||
if k == Key::Escape && !busy && !claimed && !escape_held_by_drag {
|
||||
if app.session.active().is_some_and(|d| d.doc.pattern_edit.is_some()) {
|
||||
let _ = app.run("object.pattern.done", json!({}));
|
||||
} else if app.session.active().is_some_and(|d| d.isolation.is_some()) {
|
||||
@@ -245,6 +263,16 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
// Escape pressed twice resets a rotated view (with the Rotate View tool, once).
|
||||
let _ = app.run("view.rotateReset", json!({}));
|
||||
escape_pair = None;
|
||||
} else if matches!(app.ui.screen_mode, 1 | 2)
|
||||
&& !egui::Popup::is_any_open(ctx)
|
||||
&& !text_focused_in_previous_frame(ctx)
|
||||
&& ctx.input(|i| {
|
||||
i.events.iter().any(|e| {
|
||||
matches!(e, egui::Event::Key { key: Key::Escape, pressed: true, repeat: false, modifiers, .. } if *modifiers == Modifiers::NONE)
|
||||
})
|
||||
})
|
||||
{
|
||||
let _ = app.run("view.screenMode", json!({ "mode": 0 }));
|
||||
}
|
||||
}
|
||||
// An Escape that ended something can start a pair too. One that closed a popup cannot.
|
||||
|
||||
@@ -34,13 +34,14 @@ pub(super) fn escape_press(ctx: &egui::Context) -> Option<bool> {
|
||||
}
|
||||
|
||||
/// Does this Escape reset the view's rotation? It does when the view is rotated, no popup is open,
|
||||
/// the ruler origin is not being dragged, and the press is the second of a pair (`second`) or the
|
||||
/// Rotate View tool is chosen.
|
||||
/// neither a ruler-origin drag nor a floating Tools move is ending, and the press is the second of
|
||||
/// a pair (`second`) or the Rotate View tool is chosen.
|
||||
pub(super) fn escape_resets_rotation(app: &VectorcraftApp, ctx: &egui::Context, second: bool) -> bool {
|
||||
app.view().is_some_and(|v| v.rotation != 0.0)
|
||||
&& (second || app.session.tool_id() == "rotateView")
|
||||
&& !egui::Popup::is_any_open(ctx)
|
||||
&& !crate::canvas::ruler_origin_dragging(ctx)
|
||||
&& !crate::tools_float::escape_cancelled_move(ctx)
|
||||
}
|
||||
|
||||
/// The keys in Presentation Mode, where nothing is edited: Escape leaves the mode, and ← and → go to
|
||||
|
||||
@@ -68,6 +68,465 @@ fn snapshot(app: &VectorcraftApp) -> impl Fn(&VectorcraftApp) -> bool + use<> {
|
||||
}
|
||||
}
|
||||
|
||||
fn in_screen_mode(ctx: &egui::Context, mode: u8) -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 90, "y": 40, "width": 20, "height": 20})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.select_tool("selection");
|
||||
frame(&mut app, ctx, vec![]);
|
||||
app.run("view.screenMode", json!({"mode": mode})).unwrap();
|
||||
frame(&mut app, ctx, vec![]);
|
||||
app
|
||||
}
|
||||
|
||||
fn begin_tools_move(app: &mut VectorcraftApp, ctx: &egui::Context) -> (Option<[f32; 2]>, egui::Pos2) {
|
||||
app.run("window.panel.float", json!({"panel": "tools", "x": 120, "y": 140})).unwrap();
|
||||
frame(app, ctx, vec![]);
|
||||
frame(app, ctx, vec![]);
|
||||
let original = app.ui.toolbar_pos;
|
||||
let area = ctx.memory(|m| m.area_rect(egui::Id::unique("toolbar-floating"))).expect("the floating Tools area");
|
||||
let title = area.min + egui::vec2(20.0, 17.0);
|
||||
let to = title + egui::vec2(100.0, 80.0);
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, ctx, vec![egui::Event::PointerMoved(title), button(title, true)]);
|
||||
for step in 1..=6 {
|
||||
frame(app, ctx, vec![egui::Event::PointerMoved(title + (to - title) * (step as f32 / 6.0))]);
|
||||
}
|
||||
assert_ne!(app.ui.toolbar_pos, original, "the Tools panel moved");
|
||||
(original, to)
|
||||
}
|
||||
|
||||
fn begin_ruler_origin_drag(app: &mut VectorcraftApp, ctx: &egui::Context) -> egui::Pos2 {
|
||||
app.ui.view.rulers = true;
|
||||
frame(app, ctx, vec![]);
|
||||
let corner = ctx.read_response(egui::Id::unique("ruler-corner")).expect("the rulers' corner").rect.center();
|
||||
let to = app.canvas_rect.unwrap().center();
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, ctx, vec![egui::Event::PointerMoved(corner)]);
|
||||
frame(app, ctx, vec![button(corner, true)]);
|
||||
frame(app, ctx, vec![egui::Event::PointerMoved(to)]);
|
||||
assert!(crate::canvas::ruler_origin_dragging(ctx));
|
||||
to
|
||||
}
|
||||
|
||||
fn release_drag(app: &mut VectorcraftApp, ctx: &egui::Context, to: egui::Pos2) {
|
||||
frame(
|
||||
app,
|
||||
ctx,
|
||||
vec![egui::Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }],
|
||||
);
|
||||
}
|
||||
|
||||
fn start_escape_action(app: &mut VectorcraftApp, ctx: &egui::Context, action: &str) {
|
||||
match action {
|
||||
"drawer" => app.ui.tools_drawer = true,
|
||||
"pattern" => {
|
||||
app.run("object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
}
|
||||
"isolation" => {
|
||||
app.run("shape.rectangle", json!({"x": 130, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("object.group", json!({})).unwrap();
|
||||
app.run("object.isolate", json!({})).unwrap();
|
||||
}
|
||||
"global" => {
|
||||
app.run("shape.rectangle", json!({"x": 130, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
app.run("select.globalEdit", json!({})).unwrap();
|
||||
}
|
||||
"flyout" => {
|
||||
app.ui.flyout = Some(0);
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("flyout-tools"), vec!["selection".to_string(), "directSelection".to_string()]));
|
||||
}
|
||||
_ => panic!("unknown Escape action"),
|
||||
}
|
||||
assert!(escape_action_active(app, action), "{action} starts");
|
||||
}
|
||||
|
||||
fn escape_action_active(app: &VectorcraftApp, action: &str) -> bool {
|
||||
match action {
|
||||
"drawer" => app.ui.tools_drawer,
|
||||
"pattern" => app.session.active().unwrap().doc.pattern_edit.is_some(),
|
||||
"isolation" => app.session.active().unwrap().isolation.is_some(),
|
||||
"global" => app.session.active().unwrap().global_edit.is_some(),
|
||||
"flyout" => app.ui.flyout.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_leaves_both_full_screen_modes_without_editing() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
let unchanged = snapshot(&app);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "mode {mode}");
|
||||
assert!(unchanged(&app), "mode {mode}: document, selection and undo history");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "Escape in Normal mode");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_escape_keeps_full_screen_mode() {
|
||||
for modifiers in [egui::Modifiers::SHIFT, egui::Modifiers::CTRL, egui::Modifiers::COMMAND, egui::Modifiers::ALT] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, 2);
|
||||
let events =
|
||||
[true, false].map(|pressed| egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed, repeat: false, modifiers }).to_vec();
|
||||
frame(&mut app, &ctx, events);
|
||||
assert_eq!(app.ui.screen_mode, 2, "{modifiers:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawer_and_isolation_take_escape_before_full_screen() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.ui.tools_drawer = true;
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(!app.ui.tools_drawer);
|
||||
assert_eq!(app.ui.screen_mode, mode, "drawer");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after drawer");
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.run("shape.rectangle", json!({"x": 130, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("object.group", json!({})).unwrap();
|
||||
app.run("object.isolate", json!({})).unwrap();
|
||||
assert!(app.session.active().unwrap().isolation.is_some());
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(app.session.active().unwrap().isolation.is_none());
|
||||
assert_eq!(app.ui.screen_mode, mode, "isolation");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after isolation");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_drag_takes_escape_before_full_screen_exits() {
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, 2);
|
||||
let view = app.view_info();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 50.0), view).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Drag, 120.0, 70.0), view).unwrap();
|
||||
assert!(app.session.tool_busy());
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(!app.session.tool_busy());
|
||||
assert_eq!(app.ui.screen_mode, 2);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0);
|
||||
}
|
||||
|
||||
fn navigation_drag_keeps_screen_mode(kind: &str) {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
let button = match kind {
|
||||
"hand" => {
|
||||
app.select_tool("hand");
|
||||
egui::PointerButton::Primary
|
||||
}
|
||||
"middle" => egui::PointerButton::Middle,
|
||||
"zoom_box" => {
|
||||
app.select_tool("zoom");
|
||||
app.session.prefs.animated_zoom = false;
|
||||
egui::PointerButton::Primary
|
||||
}
|
||||
_ => panic!("unknown navigation drag"),
|
||||
};
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let start = app.canvas_rect.unwrap().center();
|
||||
let end = start + egui::vec2(35.0, 20.0);
|
||||
let pointer = |pos, pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(start), pointer(start, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(end)]);
|
||||
assert!(crate::canvas::dragging(&ctx), "{kind} drag began in mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, mode, "{kind} drag holds Escape in mode {mode}");
|
||||
frame(&mut app, &ctx, vec![pointer(end, false)]);
|
||||
assert!(!crate::canvas::dragging(&ctx), "{kind} drag ended in mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "Escape after {kind} drag in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hand_drag_keeps_both_full_screen_modes_until_release() {
|
||||
navigation_drag_keeps_screen_mode("hand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_middle_button_pan_keeps_both_full_screen_modes_until_release() {
|
||||
navigation_drag_keeps_screen_mode("middle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zoom_box_drag_keeps_both_full_screen_modes_until_release() {
|
||||
navigation_drag_keeps_screen_mode("zoom_box");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ruler_origin_drag_takes_escape_before_full_screen_exits() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.ui.view.rulers = true;
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let corner = ctx.read_response(egui::Id::unique("ruler-corner")).expect("the rulers' corner").rect.center();
|
||||
let to = app.canvas_rect.unwrap().center();
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(corner)]);
|
||||
frame(&mut app, &ctx, vec![button(corner, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(to)]);
|
||||
assert!(crate::canvas::ruler_origin_dragging(&ctx), "mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, mode, "Escape cancels the drag in mode {mode}");
|
||||
frame(&mut app, &ctx, vec![button(to, false)]);
|
||||
assert!(!crate::canvas::ruler_origin_dragging(&ctx), "mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "Escape after the drag in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_floating_tools_move_takes_escape_before_full_screen_exits() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.run("window.panel.float", json!({"panel": "tools", "x": 120, "y": 140})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let original = app.ui.toolbar_pos;
|
||||
let [x, y] = original.expect("the Tools panel floats");
|
||||
let area = ctx.memory(|m| m.area_rect(egui::Id::unique("toolbar-floating"))).expect("the floating Tools area");
|
||||
let title = area.min + egui::vec2(20.0, 17.0);
|
||||
let to = title + egui::vec2(100.0, 80.0);
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(title), button(title, true)]);
|
||||
for step in 1..=6 {
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(title + (to - title) * (step as f32 / 6.0))]);
|
||||
}
|
||||
assert_ne!(app.ui.toolbar_pos, original, "the Tools panel moved in mode {mode}: saved {x} {y}, area {area:?}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, mode, "Escape cancels the Tools move in mode {mode}");
|
||||
assert_eq!(app.ui.toolbar_pos, original, "Escape restores the Tools panel's position");
|
||||
frame(&mut app, &ctx, vec![button(to, false)]);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "Escape after the move in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_tools_move_keeps_rotation_in_both_full_screen_modes() {
|
||||
for mode in [1, 2] {
|
||||
for tool in ["rotateView", "selection"] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.select_tool(tool);
|
||||
app.run("view.rotate", json!({"angle": 30})).unwrap();
|
||||
if tool == "selection" {
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "first Escape leaves mode {mode}");
|
||||
assert_eq!(app.view().unwrap().rotation, 30.0);
|
||||
}
|
||||
let (original, to) = begin_tools_move(&mut app, &ctx);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.toolbar_pos, original, "{tool} in mode {mode}");
|
||||
assert_eq!(app.view().unwrap().rotation, 30.0, "{tool} in mode {mode}");
|
||||
release_drag(&mut app, &ctx, to);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.view().unwrap().rotation, 0.0, "later Escape resets rotation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_tools_move_keeps_the_next_escape_action() {
|
||||
for mode in [0, 1, 2] {
|
||||
for action in ["drawer", "pattern", "isolation", "global", "flyout"] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
let (original, to) = begin_tools_move(&mut app, &ctx);
|
||||
start_escape_action(&mut app, &ctx, action);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.toolbar_pos, original, "move cancelled in mode {mode}, {action}");
|
||||
assert!(escape_action_active(&app, action), "Escape keeps {action} in mode {mode}");
|
||||
assert_eq!(app.ui.screen_mode, mode, "mode {mode}, {action}");
|
||||
release_drag(&mut app, &ctx, to);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(!escape_action_active(&app, action), "later Escape ends {action} in mode {mode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_ruler_origin_drag_keeps_the_next_escape_action() {
|
||||
for mode in [0, 1, 2] {
|
||||
for action in ["pattern", "isolation", "flyout"] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
let to = begin_ruler_origin_drag(&mut app, &ctx);
|
||||
start_escape_action(&mut app, &ctx, action);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(escape_action_active(&app, action), "Escape keeps {action} in mode {mode}");
|
||||
assert_eq!(app.ui.screen_mode, mode, "mode {mode}, {action}");
|
||||
release_drag(&mut app, &ctx, to);
|
||||
assert!(!crate::canvas::ruler_origin_dragging(&ctx));
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(!escape_action_active(&app, action), "later Escape ends {action} in mode {mode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_popup_takes_escape_before_full_screen_exits() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, 2);
|
||||
let pos = app.canvas_rect.unwrap().center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos), button(true)]);
|
||||
frame(&mut app, &ctx, vec![button(false)]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert!(egui::Popup::is_any_open(&ctx));
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 2);
|
||||
assert!(!egui::Popup::is_any_open(&ctx));
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rotated_view_resets_before_full_screen_exits() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, 2);
|
||||
app.select_tool("rotateView");
|
||||
app.run("view.rotate", json!({"angle": 30})).unwrap();
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.view().unwrap().rotation, 0.0);
|
||||
assert_eq!(app.ui.screen_mode, 2);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dialog_takes_escape_before_full_screen_exits() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.ui.dialog = Some(crate::state::Dialog::new("rectangle", json!({"width": 20, "height": 20})));
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(app.ui.dialog.is_none(), "mode {mode}");
|
||||
assert_eq!(app.ui.screen_mode, mode, "mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after the dialog in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_focused_field_keeps_full_screen_mode() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
let id = egui::Id::unique("screen-mode-field");
|
||||
let mut value = String::new();
|
||||
let mut field_frame = |app: &mut VectorcraftApp, events, focus| {
|
||||
let raw = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0))),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
let response = ui.add(egui::TextEdit::singleline(&mut value).id(id));
|
||||
if focus {
|
||||
response.request_focus();
|
||||
}
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
field_frame(&mut app, vec![], true);
|
||||
assert!(ctx.egui_wants_keyboard_input());
|
||||
field_frame(&mut app, escape(), false);
|
||||
assert_eq!(app.ui.screen_mode, mode, "focused field in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_claimed_tool_key_keeps_full_screen_mode() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.select_tool("intertwine");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, mode, "Intertwine claims Escape in mode {mode}");
|
||||
app.select_tool("selection");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after changing tools in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_edit_and_global_edit_take_escape_before_full_screen() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.run("object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
assert!(app.session.active().unwrap().doc.pattern_edit.is_some());
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(app.session.active().unwrap().doc.pattern_edit.is_none());
|
||||
assert_eq!(app.ui.screen_mode, mode, "pattern edit in mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after pattern edit in mode {mode}");
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.run("shape.rectangle", json!({"x": 130, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
app.run("select.globalEdit", json!({})).unwrap();
|
||||
assert!(app.session.active().unwrap().global_edit.is_some());
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(app.session.active().unwrap().global_edit.is_none());
|
||||
assert_eq!(app.ui.screen_mode, mode, "Global Edit in mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after Global Edit in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_flyout_takes_escape_before_full_screen() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.ui.flyout = Some(0);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert!(app.ui.flyout.is_none());
|
||||
assert_eq!(app.ui.screen_mode, mode, "flyout in mode {mode}");
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "after flyout in mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rotation_pair_exits_full_screen_then_resets_with_selection_tool() {
|
||||
for mode in [1, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = in_screen_mode(&ctx, mode);
|
||||
app.run("view.rotate", json!({"angle": 30})).unwrap();
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.ui.screen_mode, 0, "first Escape in mode {mode}");
|
||||
assert_eq!(app.view().unwrap().rotation, 30.0);
|
||||
frame(&mut app, &ctx, escape());
|
||||
assert_eq!(app.view().unwrap().rotation, 0.0, "second Escape after mode {mode}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_leaves_presentation_mode() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! agents. Its position is kept with the preferences and in user workspaces, and a position saved
|
||||
//! on a bigger window is clamped into this one when drawn.
|
||||
|
||||
use egui::{CornerRadius, Id, Pos2, Rect, Stroke, Vec2, pos2, vec2};
|
||||
use egui::{CornerRadius, Id, Key, Pos2, Rect, Stroke, Vec2, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
@@ -33,6 +33,14 @@ fn move_id() -> Id {
|
||||
Id::unique("tools-float-move")
|
||||
}
|
||||
|
||||
fn escape_cancel_id() -> Id {
|
||||
Id::unique("tools-float-escape-cancel")
|
||||
}
|
||||
|
||||
pub(crate) fn escape_cancelled_move(ctx: &egui::Context) -> bool {
|
||||
ctx.data(|d| d.get_temp::<u64>(escape_cancel_id())) == Some(ctx.cumulative_frame_nr_for(ctx.viewport_id()))
|
||||
}
|
||||
|
||||
/// The strip along the window's left edge where the Tools panel docks, as last laid out.
|
||||
pub(crate) fn tools_zone_id() -> Id {
|
||||
Id::unique("tools-float-zone")
|
||||
@@ -50,6 +58,13 @@ fn gesture_allowed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|input| input.focused && !input.key_pressed(egui::Key::Escape))
|
||||
}
|
||||
|
||||
/// Cancel a move before shortcuts route the same Escape press.
|
||||
pub(crate) fn cancel_move_on_escape(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if ctx.input(|i| i.key_pressed(Key::Escape)) {
|
||||
track(app, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn start(app: &VectorcraftApp, ctx: &egui::Context, grab: Vec2) {
|
||||
let Some(from) = ctx.input(|i| i.pointer.press_origin().or(i.pointer.interact_pos())) else { return };
|
||||
ctx.data_mut(|d| d.insert_temp(move_id(), Move { grab, from, original: app.ui.toolbar_pos }));
|
||||
@@ -94,6 +109,10 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if !gesture_allowed(ctx) {
|
||||
app.ui.toolbar_pos = m.original;
|
||||
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
||||
if ctx.input(|i| i.key_pressed(Key::Escape)) {
|
||||
let frame = ctx.cumulative_frame_nr_for(ctx.viewport_id());
|
||||
ctx.data_mut(|d| d.insert_temp(escape_cancel_id(), frame));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let at = ctx.input(|i| i.pointer.interact_pos());
|
||||
|
||||
@@ -759,6 +759,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard, fits it and synchronizes the Artboard tool selection when that tool is active; ordinary object selection is preserved. Actual Next-button regressions cover keyboard/Properties deletion and Undo in `chrome.rs`. Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it. Duplicate Artboards, or a row dragged onto New Artboard, copies the artboard with its art (#446). Escape leaves Presentation Mode, `view.screenMode` from it goes back to Normal Screen Mode, and the screen mode isn't saved with the preferences: the app always starts in Normal Screen Mode with its menus and panels (#472). View › Pixel Preview shows the art as the document pixels it rasterizes to, one per point and hard-edged when magnified, turning with a rotated view (it used to draw the smooth vectors, with only the pixel grid over them). In Outline view a template layer keeps drawing in Preview, dimmed, so a picture placed to be traced stays visible under the outlines (#1505, fixes #1486).
|
||||
In Presentation Mode, → and ← go to the next and the previous artboard, and a click anywhere goes to the next one, as the status bar navigator's Next and Previous do (`view.goToArtboard`): on the last artboard → and a click leave the view as it is, and so does ← on the first. The arrow keys do not move the selection there, and the tool does not receive a click, so a click selects and draws nothing (behavior reference: [View artwork](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)).
|
||||
Full Screen Mode and Presentation Mode put the window full screen, covering the taskbar and the window's frame, and Normal Screen Mode or Full Screen Mode with Menu Bar puts it back (#4365); a window the user made full screen stays so.
|
||||
An unmodified Escape in either Full Screen Mode exits to Normal Screen Mode after any open popup, focused field, tool gesture, ruler-origin drag, floating Tools panel move, edit mode, drawer, flyout or view rotation has taken its Escape; a later Escape exits when that action is finished. This is a VectorCraft choice for both full screen variants, based on the public [shortcut list](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html); the list does not specify that priority or the return mode.
|
||||
Presentation Mode shows the active artboard fitted to the screen (fitted again as the window goes full screen) and hides the selection, guides, grids, slices and the artboards' edges and names, as [presentation mode](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/view-artwork-in-presentation-mode.html) describes.
|
||||
In Presentation Mode no key press other than ←, → and Escape runs a command or reaches the tool (tool keys, Delete, Enter, Tab, F, Shift+F and every command shortcut, the macOS menu bar's key equivalents included), a right click or a long touch opens no context menu, a tap of two or three fingers undoes or redoes nothing, and the pointer over the canvas is the default one instead of the tool's cursor.
|
||||
Align to Artboard uses the active artboard, the one the Artboards panel highlights (`object.align {artboard}`). A press with a selection tool inside an artboard makes that artboard active; with the Selection tool, a Shift-click on an empty spot inside an artboard makes it active and keeps the selection. One selected object is aligned to the active artboard too, whatever Align To says (a lone object is never the key object). When an action plays, its Align to Artboard, All on Active Artboard, Paste in Place, Paste in Front and Paste in Back steps that name no artboard use the artboard that is active then.
|
||||
|
||||
Reference in New Issue
Block a user