Issue #28: retain quick selection drag endpoints and clarify fill handle
This commit is contained in:
parent
13664fe66b
commit
198e8ecb05
|
|
@ -81,7 +81,8 @@ by open equivalents or left out, not cloned.
|
|||
autocomplete and argument hints, copy/cut/paste and Paste Special (values, formats, transpose,
|
||||
operations, link, skip blanks), AutoFill series (numbers, dates, months, weekdays, custom lists,
|
||||
text+number), Fill Series, Flash Fill, find/replace with wildcards, Go To / Go To Special,
|
||||
undo/redo with history, format painter.
|
||||
undo/redo with history, format painter. Quick selection drags retain their final endpoint;
|
||||
the fill handle explains how to fill versus select cells.
|
||||
- Formatting: fonts, fills, all border styles, alignment (wrap, indent, rotation, merge), number
|
||||
formats, 47 cell styles, 60 table styles, conditional formatting (cell rules, text, dates,
|
||||
duplicates, top/bottom, averages, data bars, colour scales, icon sets, formulas), automatic
|
||||
|
|
|
|||
|
|
@ -1006,6 +1006,9 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
ctx.set_cursor_icon(CursorIcon::ResizeRow);
|
||||
} else if handle.contains(p) && app.editor.is_none() {
|
||||
ctx.set_cursor_icon(CursorIcon::Crosshair);
|
||||
if app.grid.drag == Drag::None && !pointer.any_down() {
|
||||
resp.clone().on_hover_text_at_pointer("Drag to fill cells. Drag from inside a cell to select a range.");
|
||||
}
|
||||
} else if in_cells && app.editor.is_none() && on_border(cur_rect, p) {
|
||||
ctx.set_cursor_icon(CursorIcon::Move);
|
||||
} else if in_cells {
|
||||
|
|
@ -1140,6 +1143,11 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
ed.point_cell = Some(c);
|
||||
app.grid.drag = Drag::Select;
|
||||
app.grid.drag_select = true;
|
||||
if resp.drag_started()
|
||||
&& let Some(p) = pos
|
||||
{
|
||||
select_to_pointer(app, geo, sh, wb, p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if app.editor.is_some() && !app.commit_edit(0, 0, false, false) {
|
||||
|
|
@ -1193,6 +1201,12 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
}
|
||||
app.grid.drag = Drag::Select;
|
||||
app.grid.drag_select = true;
|
||||
if resp.drag_started()
|
||||
&& let Some(p) = pos
|
||||
{
|
||||
// The first recognised drag already includes movement away from the press.
|
||||
select_to_pointer(app, geo, sh, wb, p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1221,26 +1235,7 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
);
|
||||
match app.grid.drag.clone() {
|
||||
Drag::Select => {
|
||||
if let Some(ed) = app.editor.as_mut()
|
||||
&& ed.point.is_some()
|
||||
{
|
||||
if let Some(start) = ed.point_cell {
|
||||
let r = RangeRef::new(start, pc);
|
||||
let text = if r.is_single() { r.start.a1() } else { r.a1() };
|
||||
let keep = ed.point_cell;
|
||||
ed.insert_ref(&text);
|
||||
ed.point_cell = keep;
|
||||
}
|
||||
} else {
|
||||
let anchor = sel.anchor;
|
||||
let mut ranges: Vec<String> = sel.ranges.iter().map(|r| r.a1()).collect();
|
||||
ranges.pop();
|
||||
ranges.push(RangeRef::new(anchor, pc).a1());
|
||||
let _ = app.session.run("selection.set", json!({"range": ranges.join(","), "active": sel.active.a1()}));
|
||||
if let Some(d) = app.session.active_mut() {
|
||||
d.selection.anchor = anchor;
|
||||
}
|
||||
}
|
||||
select_to_pointer(app, geo, sh, wb, p);
|
||||
scroll_by(app, auto);
|
||||
}
|
||||
Drag::Rows(anchor) => {
|
||||
|
|
@ -1313,6 +1308,11 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
// Release.
|
||||
if resp.drag_stopped() || (!pointer.any_down() && app.grid.drag != Drag::None && !resp.dragged()) {
|
||||
match std::mem::take(&mut app.grid.drag) {
|
||||
Drag::Select if resp.drag_stopped() => {
|
||||
if let Some(p) = pos {
|
||||
select_to_pointer(app, geo, sh, wb, p);
|
||||
}
|
||||
}
|
||||
Drag::Fill { target: Some(t) } => {
|
||||
let src = sel.current();
|
||||
if t != src {
|
||||
|
|
@ -1392,6 +1392,35 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||
keyboard(app, &ctx, resp, geo, sh);
|
||||
}
|
||||
|
||||
fn select_to_pointer(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, p: Pos2) {
|
||||
let cell = CellRef::new(
|
||||
geo.row_at(sh, p.y.clamp(geo.cells.top() + 1.0, geo.cells.bottom() - 1.0)),
|
||||
geo.col_at(sh, p.x.clamp(geo.cells.left() + 1.0, geo.cells.right() - 1.0)),
|
||||
);
|
||||
if let Some(ed) = app.editor.as_mut()
|
||||
&& ed.point.is_some()
|
||||
{
|
||||
if let Some(start) = ed.point_cell {
|
||||
let range = RangeRef::new(start, cell);
|
||||
let text = if range.is_single() { range.start.a1() } else { range.a1() };
|
||||
let text = if ed.sheet != wb.active_sheet { format!("{}!{text}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { text };
|
||||
ed.insert_ref(&text);
|
||||
ed.point_cell = Some(start);
|
||||
}
|
||||
} else if let Some(d) = app.session.active() {
|
||||
// Read the current anchor: starting a drag may have just replaced the selection.
|
||||
let anchor = d.selection.anchor;
|
||||
let active = d.selection.active;
|
||||
let mut ranges: Vec<String> = d.selection.ranges.iter().map(|r| r.a1()).collect();
|
||||
ranges.pop();
|
||||
ranges.push(RangeRef::new(anchor, cell).a1());
|
||||
let _ = app.session.run("selection.set", json!({"range": ranges.join(","), "active": active.a1()}));
|
||||
if let Some(d) = app.session.active_mut() {
|
||||
d.selection.anchor = anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn coalesce_last_undo(app: &mut SheetApp) {
|
||||
// Merge consecutive drag steps into one undo entry.
|
||||
if let Some(d) = app.session.active_mut()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
//! Cell selection consumes every pointer position without editing cell contents.
|
||||
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::{CellRef, RangeRef};
|
||||
use gridcraft_ui_egui::{SheetApp, grid::Geo};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
fn harness(initial: &str) -> Harness {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
session.execute("range.setValues", json!({"range": "A1", "values": [["sentinel", 2], [3, "untouched"]]})).unwrap();
|
||||
session.execute("cell.set", json!({"cell": "C3", "input": "123"})).unwrap();
|
||||
session.execute("cell.set", json!({"cell": "F7", "input": "=C3*2"})).unwrap();
|
||||
session.execute("selection.set", json!({"cell": initial})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn point(h: &Harness, a1: &str) -> Pos2 {
|
||||
let app = h.state();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll).cell_rect(sheet, CellRef::parse(a1).unwrap()).center()
|
||||
}
|
||||
|
||||
fn cells(h: &Harness) -> Value {
|
||||
let sheet = h.state().session.active().unwrap().wb.active().unwrap();
|
||||
json!(sheet.cells.iter().map(|(pos, cell)| (pos.a1(), cell)).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn frame(h: &mut Harness, event: Event) {
|
||||
h.input_mut().events.push(event);
|
||||
h.run_steps(1);
|
||||
}
|
||||
|
||||
fn button(pos: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
|
||||
}
|
||||
|
||||
fn press(h: &mut Harness, cell: &str) {
|
||||
let from = point(h, cell);
|
||||
frame(h, Event::PointerMoved(from));
|
||||
frame(h, button(from, true));
|
||||
}
|
||||
|
||||
fn assert_selection(h: &Harness, start: &str, end: &str) {
|
||||
let selection = &h.state().session.active().unwrap().selection;
|
||||
let start = CellRef::parse(start).unwrap();
|
||||
assert_eq!(selection.current(), RangeRef::new(start, CellRef::parse(end).unwrap()));
|
||||
assert_eq!(selection.anchor, start);
|
||||
assert_eq!(selection.active, start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_movement_selects_from_the_pressed_cell_in_both_directions() {
|
||||
for (initial, start, end) in [("A1", "C3", "F7"), ("C3", "C3", "F7"), ("A1", "F7", "C3"), ("F7", "F7", "C3")] {
|
||||
let mut h = harness(initial);
|
||||
let before = cells(&h);
|
||||
press(&mut h, start);
|
||||
let to = point(&h, end);
|
||||
frame(&mut h, Event::PointerMoved(to));
|
||||
assert_selection(&h, start, end);
|
||||
frame(&mut h, button(to, false));
|
||||
h.run_steps(1);
|
||||
assert_selection(&h, start, end);
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_consumes_a_new_endpoint_without_an_intermediate_movement_frame() {
|
||||
let mut h = harness("A1");
|
||||
let before = cells(&h);
|
||||
press(&mut h, "C3");
|
||||
let mid = point(&h, "D4");
|
||||
frame(&mut h, Event::PointerMoved(mid));
|
||||
let to = point(&h, "F7");
|
||||
frame(&mut h, button(to, false));
|
||||
assert_selection(&h, "C3", "F7");
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_movement_frames_keep_the_anchor_and_cell_contents() {
|
||||
let mut h = harness("A1");
|
||||
let before = cells(&h);
|
||||
press(&mut h, "C3");
|
||||
for end in ["D4", "F7", "B2"] {
|
||||
let to = point(&h, end);
|
||||
frame(&mut h, Event::PointerMoved(to));
|
||||
assert_selection(&h, "C3", end);
|
||||
}
|
||||
let to = point(&h, "B2");
|
||||
frame(&mut h, button(to, false));
|
||||
assert_selection(&h, "C3", "B2");
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_then_hovering_does_not_extend_selection() {
|
||||
let mut h = harness("A1");
|
||||
let before = cells(&h);
|
||||
press(&mut h, "C3");
|
||||
let from = point(&h, "C3");
|
||||
frame(&mut h, button(from, false));
|
||||
let to = point(&h, "F7");
|
||||
frame(&mut h, Event::PointerMoved(to));
|
||||
assert_selection(&h, "C3", "C3");
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formula_point_mode_consumes_first_movement_and_release() {
|
||||
let mut h = harness("A1");
|
||||
let before = cells(&h);
|
||||
h.state_mut().begin_edit(Some("=".into()), false);
|
||||
h.run_steps(2);
|
||||
press(&mut h, "C3");
|
||||
let mid = point(&h, "D4");
|
||||
frame(&mut h, Event::PointerMoved(mid));
|
||||
assert_eq!(h.state().editor.as_ref().unwrap().text, "=C3:D4");
|
||||
let to = point(&h, "F7");
|
||||
frame(&mut h, button(to, false));
|
||||
h.run_steps(1);
|
||||
let editor = h.state().editor.as_ref().unwrap();
|
||||
assert_eq!(editor.text, "=C3:F7");
|
||||
assert_eq!(editor.point_cell, CellRef::parse("C3"));
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formula_point_drag_keeps_the_sheet_qualifier() {
|
||||
let mut h = harness("A1");
|
||||
let before = cells(&h);
|
||||
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other sheet"})).unwrap();
|
||||
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
||||
h.state_mut().begin_edit(Some("=".into()), false);
|
||||
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Other sheet"})).unwrap();
|
||||
h.run_steps(2);
|
||||
let other_before = cells(&h);
|
||||
press(&mut h, "C3");
|
||||
let mid = point(&h, "D4");
|
||||
frame(&mut h, Event::PointerMoved(mid));
|
||||
assert_eq!(h.state().editor.as_ref().unwrap().text, "='Other sheet'!C3:D4");
|
||||
let to = point(&h, "F7");
|
||||
frame(&mut h, button(to, false));
|
||||
assert_eq!(h.state().editor.as_ref().unwrap().text, "='Other sheet'!C3:F7");
|
||||
assert_eq!(cells(&h), other_before);
|
||||
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
||||
assert_eq!(cells(&h), before);
|
||||
}
|
||||
Loading…
Reference in New Issue