diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 2ea5cfae0..06c2ca894 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -71,6 +71,16 @@ fn wrap(r: Result) -> Outcome { } } +/// Runs a command for an agent. Commands that move the selection scroll the active cell into +/// view, as the Go To dialog, the Name Box and the arrow keys do. +fn run(app: &mut SheetApp, id: &str, params: Json) -> Result { + let r = app.run(id, params); + if r.is_ok() && matches!(id, "edit.goTo" | "edit.goToSpecial" | "selection.set" | "selection.move") { + app.grid.ensure_visible = true; + } + r +} + pub fn inspect(app: &SheetApp, ctx: &egui::Context) -> Json { let r = ctx.content_rect(); let v = app.view(); @@ -171,7 +181,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - "engine.execute" | "command" | "ui.menu.invoke" => { let Some(id) = s("command").or(s("id")) else { return err("missing `command`") }; let params = p.get("params").cloned().unwrap_or(json!({})); - wrap(app.run(id, if params.is_null() { json!({}) } else { params })) + wrap(run(app, id, if params.is_null() { json!({}) } else { params })) } "engine.commands" => { let mut v: Vec = app.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); @@ -381,7 +391,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - ok(Json::Null) } // Anything else that names a command runs it. - other if gridcraft_engine::find_command(other).is_some() => wrap(app.run(other, p.clone())), + other if gridcraft_engine::find_command(other).is_some() => wrap(run(app, other, p.clone())), other => err(format!("unknown method `{other}`")), }; ctx.request_repaint(); diff --git a/crates/ui-egui/tests/selection_scroll.rs b/crates/ui-egui/tests/selection_scroll.rs index 06e0d18ce..719f1a0f2 100644 --- a/crates/ui-egui/tests/selection_scroll.rs +++ b/crates/ui-egui/tests/selection_scroll.rs @@ -1,10 +1,12 @@ //! Extending the selection from the keyboard scrolls its moving end into view, not the anchor. +//! Go To and Select over the control channel scroll the active cell into view. use egui::{Event, Key, Modifiers}; use gridcraft_engine::Session; use gridcraft_engine::core::{CellRef, RangeRef}; +use gridcraft_ui_egui::control::{Outcome, handle}; use gridcraft_ui_egui::{SheetApp, grid::Geo}; -use serde_json::json; +use serde_json::{Value as Json, json}; type Harness = egui_kittest::Harness<'static, SheetApp>; @@ -104,3 +106,39 @@ fn ctrl_shift_home_and_end_scroll_the_moving_end_into_view() { key(&mut h, Key::Home, CTRL_SHIFT); assert_extended(&h, "CV200", "A1"); } + +fn control(h: &mut Harness, method: &str, params: Json) -> Json { + let ctx = h.ctx.clone(); + let out = match handle(h.state_mut(), &ctx, method, ¶ms) { + Outcome::Done(v) => v, + Outcome::Screenshot { .. } => json!(null), + }; + h.run_steps(2); + out +} + +fn active(h: &Harness) -> CellRef { + h.state().session.active().unwrap().selection.active +} + +#[test] +fn go_to_over_the_control_channel_scrolls_the_target_into_view() { + let mut h = harness(); + assert!(!visible(&h, "Z300")); + let r = control(&mut h, "engine.execute", json!({"command": "edit.goTo", "params": {"reference": "Z300"}})); + assert_eq!(r["ok"], json!(true), "{r}"); + assert_eq!(active(&h), CellRef::parse("Z300").unwrap()); + assert!(visible(&h, "Z300"), "Go To scrolls Z300 into view"); + assert!(!visible(&h, "A1")); + // A command named as the method runs the same way. + control(&mut h, "edit.goTo", json!({"reference": "A1"})); + assert!(visible(&h, "A1") && !visible(&h, "Z300")); +} + +#[test] +fn select_over_the_control_channel_scrolls_the_active_cell_into_view() { + let mut h = harness(); + control(&mut h, "engine.execute", json!({"command": "selection.set", "params": {"range": "BZ150:CA160"}})); + assert_eq!(active(&h), CellRef::parse("BZ150").unwrap()); + assert!(visible(&h, "BZ150") && !visible(&h, "A1"), "selecting a far range scrolls its active cell into view"); +} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 7cc6ec230..6bc765aeb 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -19,7 +19,7 @@ Errors come back as `{"id": …, "ok": false, "error": "…"}`. | Method | Params | What it does | |---|---|---| -| `engine.execute` | `{command, params}` | Runs any engine command (see `engine.commands` or `gridcraft-cli commands`) | +| `engine.execute` | `{command, params}` | Runs any engine command (see `engine.commands` or `gridcraft-cli commands`). `edit.goTo`, `edit.goToSpecial`, `selection.set` and `selection.move` scroll the active cell into view, as Go To does in the window | | `engine.commands` | `{search?}` | Every command: id, label, ribbon path, shortcut, params doc, enabled; `search` keeps those whose id, label or ribbon path contains it (case-insensitive) | | `engine.journal` | | Commands run so far (replayable). Passwords (`password`, `…Password` params) are left out, so a replayed Protect Sheet has no password | | `document.inspect` | | Workbook summary: sheets, used ranges, tables, charts, names, selection, undo labels |