use cell_sheet_core::model::CellPos;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MouseTarget {
Cell(CellPos),
ColHeader(usize),
RowHeader(usize),
Outside,
}
#[derive(Debug, Clone)]
pub struct GridLayout {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub row_num_width: u16,
pub header_height: u16,
pub row_offset: usize,
pub visible_cols: Vec<(usize, u16, u16)>,
}
pub fn hit_test(layout: &GridLayout, x: u16, y: u16) -> MouseTarget {
let in_x = x >= layout.x && x < layout.x + layout.width;
let in_y = y >= layout.y && y < layout.y + layout.height;
if !in_x || !in_y {
return MouseTarget::Outside;
}
let row_num_x_end = layout.x + layout.row_num_width;
let header_y_end = layout.y + layout.header_height;
let in_gutter = x < row_num_x_end;
let in_header = y < header_y_end;
if in_gutter && in_header {
return MouseTarget::Outside;
}
if in_header {
for &(col, cx, cw) in &layout.visible_cols {
if x >= cx && x < cx + cw {
return MouseTarget::ColHeader(col);
}
}
return MouseTarget::Outside;
}
if in_gutter {
let row_offset_y = y - header_y_end;
return MouseTarget::RowHeader(layout.row_offset + row_offset_y as usize);
}
let row_offset_y = y - header_y_end;
let row = layout.row_offset + row_offset_y as usize;
for &(col, cx, cw) in &layout.visible_cols {
if x >= cx && x < cx + cw {
return MouseTarget::Cell((row, col));
}
}
MouseTarget::Outside
}
use crate::action::{Action, Mode};
use crate::app::App;
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use std::time::{Duration, Instant};
pub const DOUBLE_CLICK_MS: u64 = 400;
pub const MOUSE_SCROLL_LINES: i32 = 3;
#[derive(Debug, Default)]
pub struct MouseState {
pub drag: MouseDragState,
pub last_click: Option<LastClick>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum MouseDragState {
#[default]
Idle,
DraggingCells {
anchor: CellPos,
},
DraggingColumns {
anchor_col: usize,
},
DraggingRows {
anchor_row: usize,
},
}
#[derive(Debug, Clone, Copy)]
pub struct LastClick {
pub at: Instant,
pub pos: CellPos,
}
impl MouseState {
pub fn new() -> Self {
Self::default()
}
}
pub fn handle_mouse_event(
event: MouseEvent,
state: &mut MouseState,
_app: &App,
layout: Option<&GridLayout>,
) -> Action {
if event.modifiers.contains(KeyModifiers::SHIFT) {
return Action::Noop;
}
let layout = match layout {
Some(l) => l,
None => return Action::Noop,
};
let target = hit_test(layout, event.column, event.row);
match event.kind {
MouseEventKind::Down(MouseButton::Left) => match target {
MouseTarget::Cell(pos) => {
let now = Instant::now();
let is_double = state
.last_click
.map(|lc| {
lc.pos == pos
&& now.duration_since(lc.at) <= Duration::from_millis(DOUBLE_CLICK_MS)
})
.unwrap_or(false);
if is_double {
state.drag = MouseDragState::Idle;
state.last_click = None;
Action::ChangeMode(Mode::Insert)
} else {
state.drag = MouseDragState::DraggingCells { anchor: pos };
state.last_click = Some(LastClick { at: now, pos });
Action::MouseClickCell(pos)
}
}
MouseTarget::ColHeader(c) => {
state.drag = MouseDragState::DraggingColumns { anchor_col: c };
state.last_click = None;
Action::MouseSelectColumn(c)
}
MouseTarget::RowHeader(r) => {
state.drag = MouseDragState::DraggingRows { anchor_row: r };
state.last_click = None;
Action::MouseSelectRow(r)
}
_ => Action::Noop,
},
MouseEventKind::Drag(MouseButton::Left) => {
if state.drag == MouseDragState::Idle {
return Action::Noop;
}
let grid_bottom = layout.y + layout.height;
let grid_right = layout.x + layout.width;
let header_y_end = layout.y + layout.header_height;
let row_num_x_end = layout.x + layout.row_num_width;
match state.drag {
MouseDragState::DraggingCells { .. } => {
if event.row >= grid_bottom {
return Action::MouseScroll { dx: 0, dy: 1 };
}
if event.row < header_y_end {
return Action::MouseScroll { dx: 0, dy: -1 };
}
if event.column >= grid_right {
return Action::MouseScroll { dx: 1, dy: 0 };
}
if event.column < row_num_x_end {
return Action::MouseScroll { dx: -1, dy: 0 };
}
}
MouseDragState::DraggingColumns { .. } => {
if event.column >= grid_right {
return Action::MouseScroll { dx: 1, dy: 0 };
}
}
MouseDragState::DraggingRows { .. } => {
if event.row >= grid_bottom {
return Action::MouseScroll { dx: 0, dy: 1 };
}
}
MouseDragState::Idle => {}
}
match state.drag {
MouseDragState::DraggingCells { .. } => match target {
MouseTarget::Cell(pos) => Action::MouseDragTo(pos),
_ => Action::Noop,
},
MouseDragState::DraggingColumns { .. } => match target {
MouseTarget::ColHeader(c) | MouseTarget::Cell((_, c)) => {
Action::MouseSelectColumn(c)
}
_ => Action::Noop,
},
MouseDragState::DraggingRows { .. } => match target {
MouseTarget::RowHeader(r) | MouseTarget::Cell((r, _)) => {
Action::MouseSelectRow(r)
}
_ => Action::Noop,
},
MouseDragState::Idle => Action::Noop,
}
}
MouseEventKind::Up(MouseButton::Left) => {
state.drag = MouseDragState::Idle;
Action::Noop
}
MouseEventKind::ScrollDown => Action::MouseScroll {
dx: 0,
dy: MOUSE_SCROLL_LINES,
},
MouseEventKind::ScrollUp => Action::MouseScroll {
dx: 0,
dy: -MOUSE_SCROLL_LINES,
},
MouseEventKind::ScrollLeft => Action::MouseScroll {
dx: -MOUSE_SCROLL_LINES,
dy: 0,
},
MouseEventKind::ScrollRight => Action::MouseScroll {
dx: MOUSE_SCROLL_LINES,
dy: 0,
},
_ => Action::Noop,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> GridLayout {
GridLayout {
x: 0,
y: 1,
width: 40,
height: 10,
row_num_width: 5,
header_height: 1,
row_offset: 0,
visible_cols: vec![(0, 6, 10), (1, 17, 12), (2, 30, 8)],
}
}
#[test]
fn click_in_cell() {
assert_eq!(hit_test(&fixture(), 8, 3), MouseTarget::Cell((1, 0)));
}
#[test]
fn click_in_column_header() {
assert_eq!(hit_test(&fixture(), 18, 1), MouseTarget::ColHeader(1));
}
#[test]
fn click_in_row_header() {
assert_eq!(hit_test(&fixture(), 2, 4), MouseTarget::RowHeader(2));
}
#[test]
fn click_top_left_corner_is_outside() {
assert_eq!(hit_test(&fixture(), 2, 1), MouseTarget::Outside);
}
#[test]
fn click_outside_grid_widget() {
assert_eq!(hit_test(&fixture(), 8, 0), MouseTarget::Outside);
}
#[test]
fn click_in_padding_right_of_last_visible_col() {
assert_eq!(hit_test(&fixture(), 39, 3), MouseTarget::Outside);
}
#[test]
fn click_below_last_rendered_row() {
assert_eq!(hit_test(&fixture(), 8, 11), MouseTarget::Outside);
}
#[test]
fn click_uses_per_column_widths() {
assert_eq!(hit_test(&fixture(), 27, 5), MouseTarget::Cell((3, 1)));
}
#[test]
fn click_with_nonzero_row_offset() {
let mut layout = fixture();
layout.row_offset = 100;
assert_eq!(hit_test(&layout, 8, 2), MouseTarget::Cell((100, 0)));
}
#[test]
fn click_in_inter_column_gap_is_outside() {
assert_eq!(hit_test(&fixture(), 16, 3), MouseTarget::Outside);
}
use crate::action::Action;
use crate::app::App;
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
fn synth(kind: MouseEventKind, x: u16, y: u16) -> MouseEvent {
MouseEvent {
kind,
column: x,
row: y,
modifiers: KeyModifiers::NONE,
}
}
#[test]
fn fresh_mouse_state_is_idle() {
let s = MouseState::new();
assert_eq!(s.drag, MouseDragState::Idle);
assert!(s.last_click.is_none());
}
#[test]
fn down_left_on_cell_in_normal_emits_click() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseClickCell((1, 0))
);
assert_eq!(state.drag, MouseDragState::DraggingCells { anchor: (1, 0) });
}
#[test]
fn shift_click_is_noop_for_terminal_passthrough() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 8,
row: 3,
modifiers: KeyModifiers::SHIFT,
};
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::Noop
);
assert_eq!(state.drag, MouseDragState::Idle);
}
#[test]
fn down_left_with_no_layout_is_noop() {
let app = App::new();
let mut state = MouseState::new();
let event = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, None),
Action::Noop
);
}
#[test]
fn up_left_clears_drag_state() {
let app = App::new();
let mut state = MouseState::new();
state.drag = MouseDragState::DraggingCells { anchor: (1, 0) };
let layout = fixture();
let event = synth(MouseEventKind::Up(MouseButton::Left), 8, 3);
let _ = handle_mouse_event(event, &mut state, &app, Some(&layout));
assert_eq!(state.drag, MouseDragState::Idle);
}
#[test]
fn drag_after_down_emits_drag_to() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let down = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
handle_mouse_event(down, &mut state, &app, Some(&layout));
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 18, 5);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseDragTo((3, 1))
);
}
#[test]
fn drag_without_down_is_noop() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 18, 5);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::Noop
);
}
#[test]
fn drag_into_formula_bar_scrolls_up() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let down = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
handle_mouse_event(down, &mut state, &app, Some(&layout));
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 8, 0);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: -1 }
);
}
#[test]
fn click_then_drag_moves_cursor_to_target() {
let mut app = App::new();
app.process_action(Action::MouseClickCell((1, 0)));
app.process_action(Action::MouseDragTo((3, 2)));
assert_eq!(app.cursor, (3, 2));
}
#[test]
fn down_on_column_header_emits_select_column() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::Down(MouseButton::Left), 18, 1);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseSelectColumn(1)
);
assert_eq!(
state.drag,
MouseDragState::DraggingColumns { anchor_col: 1 }
);
}
#[test]
fn drag_in_column_mode_extends_to_other_column() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 18, 1),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 32, 5);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseSelectColumn(2)
);
}
#[test]
fn drag_in_column_mode_back_to_header_extends() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 18, 1),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 32, 1);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseSelectColumn(2)
);
}
#[test]
fn down_on_row_header_emits_select_row() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::Down(MouseButton::Left), 2, 4);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseSelectRow(2)
);
assert_eq!(state.drag, MouseDragState::DraggingRows { anchor_row: 2 });
}
#[test]
fn drag_in_row_mode_extends_to_other_row() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 2, 4),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 8, 6);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseSelectRow(4)
);
}
#[test]
fn drag_in_row_mode_back_to_gutter_extends() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 2, 4),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 2, 6);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseSelectRow(4)
);
}
#[test]
fn scroll_down_emits_positive_dy() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::ScrollDown, 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: 3 }
);
}
#[test]
fn scroll_up_emits_negative_dy() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::ScrollUp, 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: -3 }
);
}
#[test]
fn scroll_left_emits_negative_dx() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::ScrollLeft, 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: -3, dy: 0 }
);
}
#[test]
fn scroll_right_emits_positive_dx() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::ScrollRight, 8, 3);
assert_eq!(
handle_mouse_event(event, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 3, dy: 0 }
);
}
#[test]
fn scroll_does_not_move_cursor() {
let mut app = App::new();
app.cursor = (5, 2);
app.process_action(Action::MouseScroll { dx: 0, dy: 3 });
assert_eq!(app.cursor, (5, 2));
assert_eq!(app.viewport.row_offset, 3);
}
#[test]
fn scroll_up_at_top_saturates_to_zero() {
let mut app = App::new();
app.process_action(Action::MouseScroll { dx: 0, dy: -10 });
assert_eq!(app.viewport.row_offset, 0);
}
#[test]
fn drag_past_bottom_emits_scroll_down() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 8, 11);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: 1 }
);
}
#[test]
fn drag_past_top_emits_scroll_up() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 8, 1);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: -1 }
);
}
#[test]
fn drag_past_right_emits_scroll_right() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 40, 5);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 1, dy: 0 }
);
}
#[test]
fn drag_past_left_into_gutter_emits_scroll_left() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 4, 5);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: -1, dy: 0 }
);
}
#[test]
fn click_in_insert_committed_cell_then_moves() {
let mut app = App::new();
app.mode = crate::action::Mode::Insert;
app.cursor = (0, 0);
app.insert_buffer = "hello".to_string();
let pos = app.cursor;
let buf = std::mem::take(&mut app.insert_buffer);
app.process_action(Action::EditCell(pos, buf));
app.mode = crate::action::Mode::Normal;
app.process_action(Action::MouseClickCell((3, 5)));
assert_eq!(app.cursor, (3, 5));
assert_eq!(app.mode, crate::action::Mode::Normal);
assert_eq!(
app.sheet.get_cell((0, 0)).map(|c| c.raw.clone()),
Some("hello".into()),
"buffer should have been committed"
);
}
#[test]
fn click_in_visual_records_last_visual_for_gv() {
use crate::action::Mode;
use crate::mode::visual::{VisualKind, VisualState};
let mut app = App::new();
app.cursor = (4, 6);
app.mode = Mode::Visual;
let vs = VisualState::new((2, 3), VisualKind::Character);
app.record_last_visual(vs.anchor, vs.kind);
app.mode = Mode::Normal;
app.process_action(Action::MouseClickCell((9, 9)));
let lv = app.last_visual.expect("last_visual must be recorded");
assert_eq!(lv.anchor, (2, 3));
assert_eq!(lv.cursor, (4, 6));
assert_eq!(lv.kind, VisualKind::Character);
assert_eq!(app.cursor, (9, 9));
assert_eq!(app.mode, Mode::Normal);
}
#[test]
fn double_click_enters_insert_mode() {
use crate::action::Mode;
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let event = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
let _ = handle_mouse_event(event, &mut state, &app, Some(&layout));
let event2 = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
assert_eq!(
handle_mouse_event(event2, &mut state, &app, Some(&layout)),
Action::ChangeMode(Mode::Insert)
);
}
#[test]
fn click_then_different_cell_is_two_singles() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let _ = handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let event2 = synth(MouseEventKind::Down(MouseButton::Left), 18, 5);
assert_eq!(
handle_mouse_event(event2, &mut state, &app, Some(&layout)),
Action::MouseClickCell((3, 1))
);
}
#[test]
fn double_click_clears_drag_and_last_click() {
use crate::action::Mode;
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
let _ = handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
let _ = handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 8, 3),
&mut state,
&app,
Some(&layout),
);
assert_eq!(state.drag, MouseDragState::Idle);
assert!(state.last_click.is_none());
let third = synth(MouseEventKind::Down(MouseButton::Left), 8, 3);
assert_eq!(
handle_mouse_event(third, &mut state, &app, Some(&layout)),
Action::MouseClickCell((1, 0))
);
let _ = Mode::Insert;
}
#[test]
fn click_in_command_clears_prompt_and_returns_to_normal() {
use crate::action::{CommandKind, Mode};
let mut app = App::new();
app.mode = Mode::Command;
app.command.enter(CommandKind::Colon);
app.command.line = "se".into();
app.command.history_idx = Some(0);
app.command.history_scratch = "abc".into();
app.command.reset_history_browse();
app.command.clear_line();
app.mode = Mode::Normal;
app.process_action(Action::MouseClickCell((1, 1)));
assert_eq!(app.mode, Mode::Normal);
assert!(app.command.line.is_empty());
assert!(app.command.history_idx.is_none());
assert!(app.command.history_scratch.is_empty());
assert_eq!(app.cursor, (1, 1));
}
#[test]
fn double_click_after_long_insert_does_not_panic() {
use crate::action::Mode;
let mut app = App::new();
app.sheet.set_cell((0, 0), "hello world more");
app.sheet.set_cell((4, 1), "hi");
app.cursor = (4, 1);
app.process_action(Action::ChangeMode(Mode::Insert));
assert_eq!(app.insert_buffer, "hi");
let new_insert_cursor = app
.sheet
.get_cell(app.cursor)
.map(|c| c.raw.len())
.unwrap_or(0);
assert_eq!(new_insert_cursor, 2);
}
#[test]
fn column_drag_past_right_edge_scrolls_right() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 18, 1),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 40, 1);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 1, dy: 0 }
);
}
#[test]
fn row_drag_past_bottom_edge_scrolls_down() {
let app = App::new();
let mut state = MouseState::new();
let layout = fixture();
handle_mouse_event(
synth(MouseEventKind::Down(MouseButton::Left), 2, 4),
&mut state,
&app,
Some(&layout),
);
let drag = synth(MouseEventKind::Drag(MouseButton::Left), 2, 11);
assert_eq!(
handle_mouse_event(drag, &mut state, &app, Some(&layout)),
Action::MouseScroll { dx: 0, dy: 1 }
);
}
#[test]
fn select_column_keeps_cursor_at_top_not_bottom() {
let mut app = App::new();
for r in 0..5 {
app.sheet.set_cell((r, 2), "x");
}
app.process_action(Action::MouseSelectColumn(2));
assert_eq!(app.cursor, (0, 2));
}
#[test]
fn select_row_keeps_cursor_at_left_not_right() {
let mut app = App::new();
for c in 0..5 {
app.sheet.set_cell((3, c), "x");
}
app.process_action(Action::MouseSelectRow(3));
assert_eq!(app.cursor, (3, 0));
}
}