use rat_theme4::StyleName;
use rat_theme4::theme::SalsaTheme;
use rat_widget::event::{HandleEvent, MouseOnly};
use rat_widget::scrolled::{Scroll, ScrollbarPolicy};
use ratatui_widgets::block::Block;
use monospace_core::{GlyphCatalog, Offset};
use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::Style,
widgets::{StatefulWidget, Widget},
};
use ratatui_crossterm::crossterm::event::Event;
use crate::chrome::{Focus, Target};
use crate::diagram_state::{DiagramState, SubMode};
use crate::vocabulary::Reply;
pub(crate) fn draw(
area: Rect,
buf: &mut Buffer,
state: &mut DiagramState,
catalog: &GlyphCatalog,
theme: &SalsaTheme,
) {
draw_region(area, buf, theme);
let interior = inner_region(area);
state.redraw(area, interior);
draw_diagram(interior, buf, state, catalog);
draw_bars(area, buf, state);
}
pub(crate) fn handle(raw: &Event, state: &mut DiagramState, focus: Focus, target: Target) -> Reply {
match scroll(raw, state) {
Reply::Ignored => {}
reply => return reply,
}
match raw {
Event::Mouse(_) => click(target, state),
Event::Key(key) if focus == Focus::Region => crate::context_table::dispatch_key(state, key),
_ => Reply::Ignored,
}
}
fn scroll(raw: &Event, state: &mut DiagramState) -> Reply {
let area = state.region();
let answer = state.scroll_area(area).handle(raw, MouseOnly);
state.scrolled(answer)
}
fn click(target: Target, state: &mut DiagramState) -> Reply {
let Target::Interior(offset) = target else {
return Reply::Ignored;
};
let owner = state.owner(offset);
if matches!(state.sub_mode(), Some(SubMode::AttachSelect { .. })) {
if let Some(owner) = owner
&& state.focus_attach_target(owner)
{
return Reply::Repaint;
}
return Reply::Consumed;
}
if state.select(owner) {
Reply::Repaint
} else {
Reply::Consumed
}
}
fn draw_diagram(area: Rect, buf: &mut Buffer, state: &DiagramState, catalog: &GlyphCatalog) {
if area.is_empty() {
return;
}
for y in 0..area.height {
for x in 0..area.width {
let at = Offset { x, y };
let Some(glyph) = state.glyph(at, catalog) else {
continue;
};
let cell = &mut buf[(area.x + x, area.y + y)];
cell.set_symbol(glyph);
if state.is_cell_selected(at) {
cell.set_style(Style::new().reversed());
}
}
}
}
fn draw_region(area: Rect, buf: &mut Buffer, theme: &SalsaTheme) {
if area.height < 2 {
return;
}
let border: Style = theme.style(Style::CONTAINER_BORDER_FG);
Block::bordered().style(border).render(area, buf);
}
fn draw_bars(area: Rect, buf: &mut Buffer, state: &mut DiagramState) {
if area.height < 2 || area.width < 2 {
return;
}
let across = Scroll::horizontal().policy(ScrollbarPolicy::Collapse);
let down = Scroll::vertical().policy(ScrollbarPolicy::Collapse);
let (hscroll, vscroll) = state.scroll_states();
across.render(
Rect::new(area.x + 1, area.bottom() - 1, area.width - 2, 1),
buf,
hscroll,
);
down.render(
Rect::new(area.right() - 1, area.y + 1, 1, area.height - 2),
buf,
vscroll,
);
}
fn inner_region(area: Rect) -> Rect {
let Rect {
x,
y,
width,
height,
} = area;
Rect {
x: x + 1,
y: y + 1,
width: width.saturating_sub(2),
height: height.saturating_sub(2),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::diagram_state::{End, SubMode};
use crate::vocabulary::Action;
use monospace_core::{Direction, ShapeId, Terminal};
use monospace_diagram::Delta;
use monospace_editing::Command;
use ratatui_core::layout::Position;
use ratatui_crossterm::crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use std::num::NonZeroU16;
const REGION: Rect = Rect::new(0, 0, 20, 8);
const A_BOX: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" } ]
}"#;
const A_LINE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 6, "orientation": "horizontal", "stroke": "light" } ]
}"#;
const A_VERTICAL_LINE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 3, "orientation": "vertical", "stroke": "light" } ]
}"#;
const A_CONNECTOR: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "connector", "id": 1,
"from": { "at": { "kind": "point", "x": 2, "y": 1 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 10, "y": 1 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" } ]
}"#;
const AN_ATTACHED_CONNECTOR: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 8, "height": 4 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 20, "y": 0 }, "size": { "width": 8, "height": 4 }, "stroke": "light" },
{ "kind": "connector", "id": 3,
"from": { "at": { "kind": "reference", "shape": 1, "anchor": "top", "offset": { "dx": 0, "dy": 0 }, "out": 0 },
"terminal": { "kind": "arm" } },
"to": { "at": { "kind": "reference", "shape": 2, "anchor": "left", "offset": { "dx": 0, "dy": 1 }, "out": 2 },
"terminal": { "kind": "symbol", "style": "solid" } },
"stroke": "light" }
]
}"#;
const A_BOX_TOO_BIG: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 30, "height": 9 }, "stroke": "light" } ]
}"#;
fn a_state_over(json: &str) -> DiagramState {
let diagram =
monospace_description::parse(json).expect("the test's description is readable");
DiagramState::new(monospace_editing::Session::new(diagram))
}
fn paint(state: &mut DiagramState) -> Buffer {
let mut buffer = Buffer::empty(REGION);
draw(
REGION,
&mut buffer,
state,
&monospace_glyph_sets::all(),
&Config::default().theme,
);
buffer
}
fn press(button: MouseButton, column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(button),
column,
row,
modifiers: KeyModifiers::NONE,
})
}
fn click(column: u16, row: u16) -> Event {
press(MouseButton::Left, column, row)
}
fn screen(x: u16, y: u16) -> Position {
Position { x: x + 1, y: y + 1 }
}
fn handle(event: &Event, state: &mut DiagramState, focus: Focus) -> Reply {
let target = match event {
Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => {
state.target_at(Position::new(mouse.column, mouse.row))
}
_ => Target::Nothing,
};
super::handle(event, state, focus, target)
}
#[test]
fn the_bar_is_on_the_border_and_takes_no_cell_from_the_window() {
let mut state = a_state_over(A_BOX_TOO_BIG);
let buffer = paint(&mut state);
assert_eq!(
buffer[(19, 1)].symbol(),
"▲",
"the vertical bar does not begin on the right border"
);
assert_eq!(
buffer[(19, 6)].symbol(),
"▼",
"the vertical bar does not end on the right border"
);
assert_eq!(
buffer[(1, 7)].symbol(),
"◄",
"the horizontal bar does not begin on the bottom border"
);
assert_eq!(
buffer[(18, 7)].symbol(),
"►",
"the horizontal bar does not end on the bottom border"
);
assert!(
state.offset_of(screen(17, 5)).is_some(),
"the bar took the window's last column"
);
assert_eq!(
state.offset_of(screen(18, 5)),
None,
"the bar's column answered as the window"
);
}
#[test]
fn the_bar_is_hidden_when_the_content_fits() {
let mut state = a_state_over(A_BOX);
let buffer = paint(&mut state);
assert_eq!(
buffer[(19, 1)].symbol(),
"│",
"the empty right border carries a bar"
);
assert_eq!(
buffer[(1, 7)].symbol(),
"─",
"the empty bottom border carries a bar"
);
}
#[test]
fn the_corners_are_the_blocks() {
let mut state = a_state_over(A_BOX_TOO_BIG);
let buffer = paint(&mut state);
for (at, glyph) in [((0, 0), "┌"), ((19, 0), "┐"), ((0, 7), "└"), ((19, 7), "┘")] {
assert_eq!(
buffer[at].symbol(),
glyph,
"the corner at {at:?} is not the block's"
);
}
}
#[test]
fn the_click_offset_is_counted_from_the_interiors_corner() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
assert_eq!(
state.offset_of(screen(2, 1)),
Some(Offset { x: 2, y: 1 }),
"the click's own position is not the offset asked for"
);
assert_eq!(
state.offset_of(Position { x: 0, y: 0 }),
None,
"a press on the region's corner reached the window"
);
assert_eq!(
state.offset_of(Position { x: 3, y: 0 }),
None,
"a press on the region's top border reached the window"
);
assert_eq!(
state.offset_of(Position { x: 0, y: 2 }),
None,
"a press on the region's left border reached the window"
);
}
#[test]
fn a_press_of_another_button_changes_nothing() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
let at = screen(2, 1);
let _ = handle(&click(at.x, at.y), &mut state, Focus::Region);
assert!(state.selected().is_some(), "the box was not selected");
for button in [MouseButton::Right, MouseButton::Middle] {
assert_eq!(
handle(&press(button, at.x, at.y), &mut state, Focus::Region),
Reply::Ignored,
"{button:?} was the region's"
);
}
let released = Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column: at.x,
row: at.y,
modifiers: KeyModifiers::NONE,
});
assert_eq!(
handle(&released, &mut state, Focus::Region),
Reply::Ignored,
"a release was a press"
);
assert!(
state.selected().is_some(),
"a press that is not the left button dropped the selection"
);
}
fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
#[test]
fn the_arrows_are_the_regions_while_it_holds_the_keyboard() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
for (arrow, delta) in [
(KeyCode::Right, (1, 0)),
(KeyCode::Left, (-1, 0)),
(KeyCode::Up, (0, -1)),
(KeyCode::Down, (0, 1)),
] {
assert_eq!(
handle(&key(arrow), &mut state, Focus::Region),
Reply::Act(Action::Command(Command::Move {
id,
by: (delta.0, delta.1).into(),
})),
"{arrow:?} did not answer the movement"
);
}
}
#[test]
fn the_arrows_say_only_that_they_move() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
let answer = handle(&key(KeyCode::Right), &mut state, Focus::Region);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::Move {
id,
by: (1, 0).into(),
}))
);
assert_eq!(
state.selected(),
Some(id),
"the region changed the selection"
);
}
#[test]
fn arrows_are_ignored_when_nothing_is_selected() {
let mut state = a_state_over(A_BOX);
assert_eq!(state.selected(), None);
for arrow in [KeyCode::Right, KeyCode::Left, KeyCode::Up, KeyCode::Down] {
assert_eq!(
handle(&key(arrow), &mut state, Focus::Region),
Reply::Ignored,
"{arrow:?} was not ignored with nothing selected"
);
}
}
#[test]
fn a_move_is_the_arrow_and_no_modifier() {
let mut state = a_state_over(A_BOX);
for modifiers in [
KeyModifiers::SHIFT,
KeyModifiers::CONTROL,
KeyModifiers::ALT,
] {
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Right, modifiers)),
&mut state,
Focus::Region,
),
Reply::Ignored,
"a modified arrow was the region's"
);
}
for kind in [KeyEventKind::Release, KeyEventKind::Repeat] {
let event = Event::Key(KeyEvent::new_with_kind(
KeyCode::Right,
KeyModifiers::NONE,
kind,
));
assert_eq!(
handle(&event, &mut state, Focus::Region),
Reply::Ignored,
"{kind:?} was a press"
);
}
assert_eq!(
handle(&key(KeyCode::Char('x')), &mut state, Focus::Region),
Reply::Ignored,
"a key that is no arrow was the region's"
);
}
#[test]
fn shift_arrows_resize_when_a_box_is_selected() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
let at = screen(2, 1);
let _ = handle(&click(at.x, at.y), &mut state, Focus::Region);
let id = state.selected().expect("box is selected");
for (arrow, delta) in [
(KeyCode::Right, (1, 0)),
(KeyCode::Left, (-1, 0)),
(KeyCode::Up, (0, -1)),
(KeyCode::Down, (0, 1)),
] {
assert_eq!(
handle(
&Event::Key(KeyEvent::new(arrow, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Act(Action::Command(Command::Resize {
id,
by: (delta.0, delta.1).into(),
})),
"{arrow:?} with Shift did not answer the resize"
);
}
}
#[test]
fn shift_arrows_resize_a_horizontal_line_along_its_row() {
let mut state = a_state_over(A_LINE);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Act(Action::Command(Command::Resize {
id,
by: (-1, 0).into(),
}))
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Act(Action::Command(Command::Resize {
id,
by: (1, 0).into(),
}))
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
}
#[test]
fn shift_arrows_resize_a_vertical_line_along_its_column() {
let mut state = a_state_over(A_VERTICAL_LINE);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Act(Action::Command(Command::Resize {
id,
by: (0, -1).into(),
}))
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Act(Action::Command(Command::Resize {
id,
by: (0, 1).into(),
}))
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
}
#[test]
fn shift_arrows_are_ignored_when_a_connector_is_selected() {
let mut state = a_state_over(A_CONNECTOR);
state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
}
#[test]
fn shift_arrows_are_ignored_when_nothing_is_selected() {
let mut state = a_state_over(A_BOX);
assert_eq!(state.selected(), None);
assert_eq!(
handle(
&Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT)),
&mut state,
Focus::Region,
),
Reply::Ignored
);
}
#[test]
fn an_arrow_with_the_bar_focused_moves_nothing() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
let _ = handle(&click(3, 2), &mut state, Focus::Region);
assert!(state.selected().is_some(), "the box was not selected");
for arrow in [KeyCode::Up, KeyCode::Down] {
assert_eq!(
handle(&key(arrow), &mut state, Focus::Bar),
Reply::Ignored,
"{arrow:?} reached the region while the bar held the keyboard"
);
}
assert!(state.selected().is_some(), "the box lost its selection");
}
#[test]
fn pressing_esc_in_the_region_clears_the_selection_and_requests_repaint() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
let _ = handle(&click(3, 2), &mut state, Focus::Region);
assert!(state.selected().is_some(), "the box was not selected");
let outcome = handle(&key(KeyCode::Esc), &mut state, Focus::Region);
assert_eq!(outcome, Reply::Repaint);
assert_eq!(state.selected(), None);
}
#[test]
fn pressing_esc_with_nothing_selected_is_ignored() {
let mut state = a_state_over(A_BOX);
assert_eq!(state.selected(), None);
let outcome = handle(&key(KeyCode::Esc), &mut state, Focus::Region);
assert_eq!(outcome, Reply::Ignored);
assert_eq!(state.selected(), None);
}
#[test]
fn escape_with_the_bar_focused_does_not_drop_the_selection() {
let mut state = a_state_over(A_BOX);
let _ = paint(&mut state);
let _ = handle(&click(3, 2), &mut state, Focus::Region);
assert!(state.selected().is_some(), "the box was not selected");
let outcome = handle(&key(KeyCode::Esc), &mut state, Focus::Bar);
assert_eq!(outcome, Reply::Ignored);
assert!(state.selected().is_some(), "the box lost its selection");
}
const NEVER_AN_ACTION: [(KeyCode, KeyModifiers); 4] = [
(KeyCode::Char('x'), KeyModifiers::NONE),
(KeyCode::Char('a'), KeyModifiers::NONE),
(KeyCode::Enter, KeyModifiers::NONE),
(KeyCode::F(1), KeyModifiers::NONE),
];
fn assert_offer_matches_acceptance(
fixture: &str,
selected: Option<u16>,
sub_mode: Option<crate::diagram_state::SubMode>,
) {
let make_state = || {
let mut state = a_state_over(fixture);
if let Some(id) = selected {
state.select(Some(ShapeId::new(NonZeroU16::new(id).unwrap())));
}
if let Some(mode) = sub_mode {
state.set_sub_mode(Some(mode));
}
state
};
let state = make_state();
let bar = crate::status::command_line(&state);
let mut offered_keys = Vec::new();
for block in crate::context_table::context_blocks(&state) {
if let Some(category) = block.category {
assert!(
bar.contains(category),
"bar missing category {category} in: {bar}"
);
}
for action in block.actions {
assert!(
bar.contains(&action.text()),
"bar missing action display {:?} in: {bar}",
action.text()
);
for (code, modifiers) in action.trigger.samples() {
offered_keys.push((code, modifiers));
let mut s = make_state();
let event = Event::Key(KeyEvent::new(code, modifiers));
let reply = handle(&event, &mut s, Focus::Region);
assert_ne!(
reply,
Reply::Ignored,
"offered key {code:?} with modifiers {modifiers:?} in context with bar {bar:?} was ignored"
);
}
}
}
let universe = crate::context_table::all_offered_samples();
let shortcut_keys = crate::shortcuts::events_unclaimed()
.into_iter()
.filter_map(|event| match event {
Event::Key(key) => Some((key.code, key.modifiers)),
_ => None,
});
for (code, modifiers) in universe.into_iter().chain(shortcut_keys) {
if !offered_keys.contains(&(code, modifiers)) {
let mut s = make_state();
let event = Event::Key(KeyEvent::new(code, modifiers));
let reply = handle(&event, &mut s, Focus::Region);
assert_eq!(
reply,
Reply::Ignored,
"unoffered key {code:?} with modifiers {modifiers:?} in context with bar {bar:?} answered unexpected reply: {reply:?}"
);
}
}
for (code, modifiers) in NEVER_AN_ACTION {
let mut s = make_state();
let event = Event::Key(KeyEvent::new(code, modifiers));
let reply = handle(&event, &mut s, Focus::Region);
assert_eq!(
reply,
Reply::Ignored,
"{code:?} with modifiers {modifiers:?} was unexpected in: {bar}"
);
}
}
#[test]
fn key_acceptance_matches_contextual_bar_in_idle() {
assert_offer_matches_acceptance(A_BOX, None, None);
}
#[test]
fn key_acceptance_matches_contextual_bar_for_selected_box() {
assert_offer_matches_acceptance(A_BOX, Some(1), None);
}
#[test]
fn key_acceptance_matches_contextual_bar_for_selected_horizontal_line() {
assert_offer_matches_acceptance(A_LINE, Some(1), None);
}
#[test]
fn key_acceptance_matches_contextual_bar_for_selected_vertical_line() {
assert_offer_matches_acceptance(A_VERTICAL_LINE, Some(1), None);
}
#[test]
fn key_acceptance_matches_contextual_bar_for_selected_connector() {
assert_offer_matches_acceptance(A_CONNECTOR, Some(1), None);
}
#[test]
fn pressing_i_inverts_connector_terminals_when_selected() {
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::InvertTerminals { id }))
);
let mut box_state = a_state_over(A_BOX);
box_state.select(Some(id));
let box_answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)),
&mut box_state,
Focus::Region,
);
assert_eq!(box_answer, Reply::Ignored);
let mut idle_state = a_state_over(A_CONNECTOR);
let idle_answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)),
&mut idle_state,
Focus::Region,
);
assert_eq!(idle_answer, Reply::Ignored);
}
#[test]
fn pressing_r_rotates_selected_line_and_is_ignored_otherwise() {
let mut state = a_state_over(A_LINE);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::RotateLine { id }))
);
let mut box_state = a_state_over(A_BOX);
box_state.select(Some(id));
let box_answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)),
&mut box_state,
Focus::Region,
);
assert_eq!(box_answer, Reply::Ignored);
let mut conn_state = a_state_over(A_CONNECTOR);
conn_state.select(Some(id));
let conn_answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)),
&mut conn_state,
Focus::Region,
);
assert_eq!(conn_answer, Reply::Ignored);
let mut idle_state = a_state_over(A_LINE);
let idle_answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)),
&mut idle_state,
Focus::Region,
);
assert_eq!(idle_answer, Reply::Ignored);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_stroke_select() {
assert_offer_matches_acceptance(
A_BOX,
Some(1),
Some(crate::diagram_state::SubMode::StrokeSelect),
);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_order_mode() {
assert_offer_matches_acceptance(
A_BOX,
Some(1),
Some(crate::diagram_state::SubMode::OrderMode),
);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_endpoint_sub_mode() {
assert_offer_matches_acceptance(A_CONNECTOR, Some(1), Some(SubMode::Endpoint(End::From)));
assert_offer_matches_acceptance(A_CONNECTOR, Some(1), Some(SubMode::Endpoint(End::To)));
assert_offer_matches_acceptance(
AN_ATTACHED_CONNECTOR,
Some(3),
Some(SubMode::Endpoint(End::From)),
);
assert_offer_matches_acceptance(
AN_ATTACHED_CONNECTOR,
Some(3),
Some(SubMode::Endpoint(End::To)),
);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_terminal_select_sub_mode() {
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(SubMode::TerminalSelect(End::From)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(SubMode::TerminalSelect(End::To)),
);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_leaving_select_sub_mode() {
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(SubMode::LeavingSelect(End::From)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(SubMode::LeavingSelect(End::To)),
);
}
#[test]
fn endpoint_sub_mode_moves_adjusts_out_and_resets_endpoints() {
let mut state = a_state_over(AN_ATTACHED_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let right_arrow = key(KeyCode::Right);
assert_eq!(
handle(&right_arrow, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::From,
by: Delta { dx: 1, dy: 0 },
out: 0,
}))
);
let up_arrow = key(KeyCode::Up);
assert_eq!(
handle(&up_arrow, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::From,
by: Delta { dx: 0, dy: -1 },
out: 0,
}))
);
let key_plus = key(KeyCode::Char('+'));
assert_eq!(
handle(&key_plus, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::From,
by: Delta::ZERO,
out: 1,
}))
);
let key_minus = key(KeyCode::Char('-'));
assert_eq!(
handle(&key_minus, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::From,
by: Delta::ZERO,
out: -1,
}))
);
let key_r = key(KeyCode::Char('r'));
assert_eq!(handle(&key_r, &mut state, Focus::Region), Reply::Ignored);
state.set_sub_mode(Some(SubMode::Endpoint(End::To)));
let down_arrow = key(KeyCode::Down);
assert_eq!(
handle(&down_arrow, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::To,
by: Delta { dx: 0, dy: 1 },
out: 0,
}))
);
assert_eq!(
handle(&right_arrow, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id,
end: End::To,
by: Delta { dx: 1, dy: 0 },
out: 0,
}))
);
assert_eq!(
handle(&key_r, &mut state, Focus::Region),
Reply::Act(Action::Command(Command::ResetEndpoint { id, end: End::To }))
);
let mut detached_state = a_state_over(A_CONNECTOR);
let conn_id = ShapeId::new(NonZeroU16::new(1).unwrap());
detached_state.select(Some(conn_id));
detached_state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
assert_eq!(
handle(&up_arrow, &mut detached_state, Focus::Region),
Reply::Act(Action::Command(Command::MoveEndpoint {
id: conn_id,
end: End::From,
by: Delta { dx: 0, dy: -1 },
out: 0,
}))
);
assert_eq!(
handle(&key_plus, &mut detached_state, Focus::Region),
Reply::Ignored
);
assert_eq!(
handle(&key_minus, &mut detached_state, Focus::Region),
Reply::Ignored
);
assert_eq!(
handle(&key_r, &mut detached_state, Focus::Region),
Reply::Ignored
);
}
#[test]
fn pressing_0_on_attached_endpoint_dispatches_detach_endpoint() {
let mut state = a_state_over(AN_ATTACHED_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let bar = crate::status::command_line(&state);
assert!(bar.contains("[0] Detach"), "bar was: {bar}");
let answer = handle(&key(KeyCode::Char('0')), &mut state, Focus::Region);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::DetachEndpoint {
id,
end: End::From
}))
);
let mut detached_state = a_state_over(A_CONNECTOR);
detached_state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
detached_state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let answer = handle(&key(KeyCode::Char('0')), &mut detached_state, Focus::Region);
assert_eq!(answer, Reply::Ignored);
}
#[test]
fn pressing_1_and_2_enters_and_switches_endpoint_sub_mode_until_esc() {
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
let key_1 = key(KeyCode::Char('1'));
let key_2 = key(KeyCode::Char('2'));
let key_esc = key(KeyCode::Esc);
let answer = handle(&key_1, &mut state, Focus::Region);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
assert_eq!(state.selected(), Some(id));
let answer = handle(&key_1, &mut state, Focus::Region);
assert_eq!(answer, Reply::Ignored);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
let answer = handle(&key_2, &mut state, Focus::Region);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::To)));
assert_eq!(state.selected(), Some(id));
let answer = handle(&key_2, &mut state, Focus::Region);
assert_eq!(answer, Reply::Ignored);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::To)));
let answer = handle(&key_1, &mut state, Focus::Region);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
assert_eq!(state.selected(), Some(id));
let answer = handle(&key_esc, &mut state, Focus::Region);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), None);
assert_eq!(state.selected(), Some(id));
assert!(!state.session().can_undo());
let mut box_state = a_state_over(A_BOX);
box_state.select(Some(id));
assert_eq!(
handle(&key_1, &mut box_state, Focus::Region),
Reply::Ignored
);
assert_eq!(box_state.sub_mode(), None);
assert_eq!(
handle(&key_2, &mut box_state, Focus::Region),
Reply::Ignored
);
assert_eq!(box_state.sub_mode(), None);
let mut line_state = a_state_over(A_LINE);
line_state.select(Some(id));
assert_eq!(
handle(&key_1, &mut line_state, Focus::Region),
Reply::Ignored
);
assert_eq!(line_state.sub_mode(), None);
assert_eq!(
handle(&key_2, &mut line_state, Focus::Region),
Reply::Ignored
);
assert_eq!(line_state.sub_mode(), None);
let mut idle_state = a_state_over(A_CONNECTOR);
assert_eq!(
handle(&key_1, &mut idle_state, Focus::Region),
Reply::Ignored
);
assert_eq!(idle_state.sub_mode(), None);
assert_eq!(
handle(&key_2, &mut idle_state, Focus::Region),
Reply::Ignored
);
assert_eq!(idle_state.sub_mode(), None);
}
#[test]
fn endpoint_sub_mode_offers_terminal_t_action() {
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let bar = crate::status::command_line(&state);
assert!(bar.contains("TERMINAL: [t]"), "bar was: {bar}");
let answer = handle(&key(KeyCode::Char('t')), &mut state, Focus::Region);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::TerminalSelect(End::From)));
}
#[test]
fn pressing_t_enters_terminal_select_and_esc_returns_to_endpoint() {
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let answer_t = handle(&key(KeyCode::Char('t')), &mut state, Focus::Region);
assert_eq!(answer_t, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::TerminalSelect(End::From)));
let answer_esc = handle(&key(KeyCode::Esc), &mut state, Focus::Region);
assert_eq!(answer_esc, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
assert_eq!(state.selected(), Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::To)));
let answer_t_to = handle(&key(KeyCode::Char('t')), &mut state, Focus::Region);
assert_eq!(answer_t_to, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::TerminalSelect(End::To)));
let answer_esc_to = handle(&key(KeyCode::Esc), &mut state, Focus::Region);
assert_eq!(answer_esc_to, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::To)));
assert_eq!(state.selected(), Some(id));
}
#[test]
fn terminal_select_dispatches_set_endpoint_terminal_and_returns_to_endpoint() {
use monospace_core::SymbolStyleName;
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
let cases = [
('1', Terminal::Arm),
(
'2',
Terminal::Symbol {
style: SymbolStyleName::from("solid"),
},
),
(
'3',
Terminal::Symbol {
style: SymbolStyleName::from("outline"),
},
),
(
'4',
Terminal::Symbol {
style: SymbolStyleName::from("ascii"),
},
),
(
'5',
Terminal::Glyph {
glyph: monospace_core::Glyph::new("◆").unwrap(),
},
),
];
for (key_char, expected_terminal) in cases {
state.set_sub_mode(Some(SubMode::TerminalSelect(End::From)));
let answer = handle(&key(KeyCode::Char(key_char)), &mut state, Focus::Region);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::SetEndpointTerminal {
id,
end: End::From,
terminal: expected_terminal.clone(),
}))
);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
state.set_sub_mode(Some(SubMode::TerminalSelect(End::To)));
let answer_to = handle(&key(KeyCode::Char(key_char)), &mut state, Focus::Region);
assert_eq!(
answer_to,
Reply::Act(Action::Command(Command::SetEndpointTerminal {
id,
end: End::To,
terminal: expected_terminal,
}))
);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::To)));
}
}
#[test]
fn pressing_d_enters_leaving_select_and_1_to_4_or_esc_returns_to_endpoint() {
let mut state = a_state_over(A_CONNECTOR);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let bar = crate::status::command_line(&state);
assert!(bar.contains("LEAVING: [d]"), "bar was: {bar}");
let answer_d = handle(&key(KeyCode::Char('d')), &mut state, Focus::Region);
assert_eq!(answer_d, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::LeavingSelect(End::From)));
let answer_esc = handle(&key(KeyCode::Esc), &mut state, Focus::Region);
assert_eq!(answer_esc, Reply::Repaint);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
assert_eq!(state.selected(), Some(id));
let cases = [
('1', Direction::Up),
('2', Direction::Right),
('3', Direction::Down),
('4', Direction::Left),
];
for (ch, direction) in cases {
state.set_sub_mode(Some(SubMode::LeavingSelect(End::From)));
let answer = handle(&key(KeyCode::Char(ch)), &mut state, Focus::Region);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::SetEndpointLeaving {
id,
end: End::From,
leaving: direction,
}))
);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::From)));
state.set_sub_mode(Some(SubMode::LeavingSelect(End::To)));
let answer_to = handle(&key(KeyCode::Char(ch)), &mut state, Focus::Region);
assert_eq!(
answer_to,
Reply::Act(Action::Command(Command::SetEndpointLeaving {
id,
end: End::To,
leaving: direction,
}))
);
assert_eq!(state.sub_mode(), Some(SubMode::Endpoint(End::To)));
}
let mut attached_state = a_state_over(AN_ATTACHED_CONNECTOR);
attached_state.select(Some(ShapeId::new(NonZeroU16::new(3).unwrap())));
attached_state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
let answer = handle(&key(KeyCode::Char('d')), &mut attached_state, Focus::Region);
assert_eq!(answer, Reply::Ignored);
}
#[test]
fn arrows_are_ignored_when_the_selection_names_a_figure_the_drawing_does_not_record() {
let mut state = a_state_over(A_BOX);
state.select(Some(ShapeId::new(NonZeroU16::new(999).unwrap())));
assert_eq!(state.selected_shape(), None);
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored);
}
#[test]
fn pressing_s_on_selected_shape_enters_stroke_select_sub_mode() {
let mut state = a_state_over(A_BOX);
state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Repaint);
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
}
#[test]
fn pressing_s_in_idle_is_ignored() {
let mut state = a_state_over(A_BOX);
assert_eq!(state.selected(), None);
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored);
assert_eq!(state.sub_mode(), None);
}
#[test]
fn digits_in_stroke_select_dispatch_set_stroke_and_clear_sub_mode() {
use monospace_diagram::Stroke;
let pairs = [
('1', "light"),
('2', "double"),
('3', "light-round"),
('4', "heavy"),
('5', "ascii"),
];
for (c, stroke_name) in pairs {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::StrokeSelect));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::SetStroke {
id,
stroke: Stroke::from(stroke_name),
}))
);
assert_eq!(state.sub_mode(), None);
}
}
#[test]
fn escape_in_stroke_select_cancels_sub_mode_and_leaves_figure_selected() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::StrokeSelect));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), None);
assert_eq!(state.selected(), Some(id));
}
#[test]
fn unoffered_keys_in_stroke_select_are_ignored() {
let mut state = a_state_over(A_BOX);
state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
state.set_sub_mode(Some(crate::diagram_state::SubMode::StrokeSelect));
let unoffered = [
KeyCode::Char('x'),
KeyCode::Char('a'),
KeyCode::Right,
KeyCode::Delete,
KeyCode::Enter,
KeyCode::Tab,
KeyCode::Char('6'),
];
for code in unoffered {
let answer = handle(
&Event::Key(KeyEvent::new(code, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored, "{code:?} was not ignored");
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
}
}
#[test]
fn unclaimed_global_shortcuts_in_stroke_select_are_ignored() {
let mut state = a_state_over(A_BOX);
state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
state.set_sub_mode(Some(crate::diagram_state::SubMode::StrokeSelect));
let global_shortcuts = [
(KeyCode::Char('z'), KeyModifiers::CONTROL),
(KeyCode::Char('s'), KeyModifiers::CONTROL),
(KeyCode::Char('q'), KeyModifiers::CONTROL),
(KeyCode::Char('n'), KeyModifiers::CONTROL),
(KeyCode::Char('o'), KeyModifiers::CONTROL),
];
for (code, modifiers) in global_shortcuts {
let answer = handle(
&Event::Key(KeyEvent::new(code, modifiers)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored, "{code:?} was not ignored");
}
}
#[test]
fn pressing_z_on_a_selected_figure_enters_order_mode() {
for fixture in [A_BOX, A_LINE, A_CONNECTOR] {
let mut state = a_state_over(fixture);
state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Repaint);
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
}
#[test]
fn pressing_z_in_idle_is_ignored() {
let mut state = a_state_over(A_BOX);
assert_eq!(state.selected(), None);
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored);
assert_eq!(state.sub_mode(), None);
}
#[test]
fn page_up_in_order_mode_dispatches_forward() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Act(Action::Command(Command::Forward { id })));
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
#[test]
fn page_down_in_order_mode_dispatches_backward() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(
answer,
Reply::Act(Action::Command(Command::Backward { id }))
);
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
#[test]
fn home_in_order_mode_dispatches_to_front() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Act(Action::Command(Command::ToFront { id })));
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
#[test]
fn end_in_order_mode_dispatches_to_back() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::End, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Act(Action::Command(Command::ToBack { id })));
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
#[test]
fn escape_in_order_mode_leaves_the_sub_mode_and_keeps_the_selection() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let answer = handle(
&Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Repaint);
assert_eq!(state.sub_mode(), None);
assert_eq!(state.selected(), Some(id));
}
#[test]
fn unoffered_keys_in_order_mode_are_ignored() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id));
state.set_sub_mode(Some(crate::diagram_state::SubMode::OrderMode));
let unoffered = [
KeyCode::Right,
KeyCode::Left,
KeyCode::Delete,
KeyCode::Enter,
KeyCode::Tab,
KeyCode::Char('s'),
KeyCode::Char('z'),
KeyCode::Char('6'),
KeyCode::F(1),
];
for code in unoffered {
let answer = handle(
&Event::Key(KeyEvent::new(code, KeyModifiers::NONE)),
&mut state,
Focus::Region,
);
assert_eq!(answer, Reply::Ignored, "{code:?} was not ignored");
assert_eq!(
state.sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
}
}
#[test]
fn tab_and_shift_tab_cycle_selection_in_drawing_order_and_wrap() {
const THREE_SHAPES: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 }, "size": { "width": 10, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 16, "y": 0 }, "size": { "width": 10, "height": 3 }, "stroke": "light" },
{ "kind": "connector", "id": 3,
"from": { "at": { "kind": "reference", "shape": 1, "anchor": "right" }, "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "reference", "shape": 2, "anchor": "left" }, "terminal": { "kind": "symbol", "style": "solid" } },
"stroke": "light" }
]
}"#;
let mut state = a_state_over(THREE_SHAPES);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let id3 = ShapeId::new(NonZeroU16::new(3).unwrap());
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let back_tab = Event::Key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id1));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id2));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id3));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id1));
assert_eq!(handle(&back_tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id3));
assert_eq!(handle(&back_tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id2));
state.select(None);
assert_eq!(handle(&back_tab, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), Some(id3));
assert!(!state.can_undo());
}
#[test]
fn tab_and_shift_tab_on_empty_diagram_or_active_sub_mode_are_ignored() {
const EMPTY: &str = r#"{ "next_id": 1, "shapes": [] }"#;
let mut empty = a_state_over(EMPTY);
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let back_tab = Event::Key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(handle(&tab, &mut empty, Focus::Region), Reply::Ignored);
assert_eq!(handle(&back_tab, &mut empty, Focus::Region), Reply::Ignored);
assert_eq!(empty.selected(), None);
let mut state = a_state_over(A_CONNECTOR);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
let sub_modes = [
crate::diagram_state::SubMode::StrokeSelect,
crate::diagram_state::SubMode::OrderMode,
crate::diagram_state::SubMode::Endpoint(crate::diagram_state::End::From),
crate::diagram_state::SubMode::Endpoint(crate::diagram_state::End::To),
crate::diagram_state::SubMode::TerminalSelect(crate::diagram_state::End::From),
crate::diagram_state::SubMode::LeavingSelect(crate::diagram_state::End::From),
];
for mode in sub_modes {
state.select(Some(id1));
state.set_sub_mode(Some(mode));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Ignored);
assert_eq!(handle(&back_tab, &mut state, Focus::Region), Reply::Ignored);
assert_eq!(state.selected(), Some(id1));
assert_eq!(state.sub_mode(), Some(mode));
}
}
#[test]
fn tab_on_single_selected_shape_is_consumed_without_history_step() {
let mut state = a_state_over(A_BOX);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id1));
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let back_tab = Event::Key(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT));
assert_eq!(handle(&tab, &mut state, Focus::Region), Reply::Consumed);
assert_eq!(state.selected(), Some(id1));
assert!(!state.can_undo());
assert_eq!(
handle(&back_tab, &mut state, Focus::Region),
Reply::Consumed
);
assert_eq!(state.selected(), Some(id1));
assert!(!state.can_undo());
}
#[test]
fn guardian_offer_equals_acceptance_across_idle_selected_and_sub_modes() {
const EMPTY: &str = r#"{ "next_id": 1, "shapes": [] }"#;
assert_offer_matches_acceptance(EMPTY, None, None);
assert_offer_matches_acceptance(A_BOX, None, None);
assert_offer_matches_acceptance(A_BOX, Some(1), None);
assert_offer_matches_acceptance(A_LINE, Some(1), None);
assert_offer_matches_acceptance(A_VERTICAL_LINE, Some(1), None);
assert_offer_matches_acceptance(A_CONNECTOR, Some(1), None);
assert_offer_matches_acceptance(
A_BOX,
Some(1),
Some(crate::diagram_state::SubMode::StrokeSelect),
);
assert_offer_matches_acceptance(
A_BOX,
Some(1),
Some(crate::diagram_state::SubMode::OrderMode),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::Endpoint(
crate::diagram_state::End::From,
)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::Endpoint(
crate::diagram_state::End::To,
)),
);
assert_offer_matches_acceptance(
AN_ATTACHED_CONNECTOR,
Some(3),
Some(crate::diagram_state::SubMode::Endpoint(
crate::diagram_state::End::From,
)),
);
assert_offer_matches_acceptance(
AN_ATTACHED_CONNECTOR,
Some(3),
Some(crate::diagram_state::SubMode::Endpoint(
crate::diagram_state::End::To,
)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::TerminalSelect(
crate::diagram_state::End::From,
)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::TerminalSelect(
crate::diagram_state::End::To,
)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::LeavingSelect(
crate::diagram_state::End::From,
)),
);
assert_offer_matches_acceptance(
A_CONNECTOR,
Some(1),
Some(crate::diagram_state::SubMode::LeavingSelect(
crate::diagram_state::End::To,
)),
);
}
#[test]
fn d_and_0_are_tested_in_guardian_and_ignored_where_they_do_not_belong() {
let universe = crate::context_table::all_offered_samples();
assert!(universe.contains(&(KeyCode::Char('d'), KeyModifiers::NONE)));
assert!(universe.contains(&(KeyCode::Char('0'), KeyModifiers::NONE)));
let mut idle_state = a_state_over(A_BOX);
assert_eq!(
handle(&key(KeyCode::Char('d')), &mut idle_state, Focus::Region),
Reply::Ignored
);
assert_eq!(
handle(&key(KeyCode::Char('0')), &mut idle_state, Focus::Region),
Reply::Ignored
);
let mut box_state = a_state_over(A_BOX);
box_state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
assert_eq!(
handle(&key(KeyCode::Char('d')), &mut box_state, Focus::Region),
Reply::Repaint
);
assert_eq!(
handle(&key(KeyCode::Char('0')), &mut box_state, Focus::Region),
Reply::Ignored
);
box_state.set_sub_mode(Some(SubMode::StrokeSelect));
assert_eq!(
handle(&key(KeyCode::Char('d')), &mut box_state, Focus::Region),
Reply::Ignored
);
box_state.set_sub_mode(Some(SubMode::OrderMode));
assert_eq!(
handle(&key(KeyCode::Char('d')), &mut box_state, Focus::Region),
Reply::Ignored
);
let mut detached_state = a_state_over(A_CONNECTOR);
detached_state.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
detached_state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
assert_ne!(
handle(&key(KeyCode::Char('d')), &mut detached_state, Focus::Region),
Reply::Ignored
);
assert_eq!(
handle(&key(KeyCode::Char('0')), &mut detached_state, Focus::Region),
Reply::Ignored
);
let mut attached_state = a_state_over(AN_ATTACHED_CONNECTOR);
attached_state.select(Some(ShapeId::new(NonZeroU16::new(3).unwrap())));
attached_state.set_sub_mode(Some(SubMode::Endpoint(End::From)));
assert_eq!(
handle(&key(KeyCode::Char('d')), &mut attached_state, Focus::Region),
Reply::Ignored
);
assert_ne!(
handle(&key(KeyCode::Char('0')), &mut attached_state, Focus::Region),
Reply::Ignored
);
}
#[test]
fn pressing_b_l_c_in_idle_creates_shapes_selects_and_repaints() {
use monospace_diagram::Shape;
const EMPTY: &str = r#"{ "next_id": 1, "shapes": [] }"#;
let mut state = a_state_over(EMPTY);
let b = Event::Key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
let l = Event::Key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
let c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
let esc = Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert_eq!(handle(&b, &mut state, Focus::Region), Reply::Repaint);
let box_id = state.selected().expect("shape selected after b");
assert!(matches!(state.selected_shape(), Some(Shape::Box { .. })));
assert_eq!(state.sub_mode(), None);
assert_eq!(handle(&esc, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), None);
assert_eq!(handle(&l, &mut state, Focus::Region), Reply::Repaint);
let line_id = state.selected().expect("shape selected after l");
assert_ne!(line_id, box_id);
assert!(matches!(state.selected_shape(), Some(Shape::Line { .. })));
assert_eq!(state.sub_mode(), None);
assert_eq!(handle(&esc, &mut state, Focus::Region), Reply::Repaint);
assert_eq!(state.selected(), None);
assert_eq!(handle(&c, &mut state, Focus::Region), Reply::Repaint);
let conn_id = state.selected().expect("shape selected after c");
assert_ne!(conn_id, line_id);
assert!(matches!(
state.selected_shape(),
Some(Shape::Connector { .. })
));
assert_eq!(state.sub_mode(), None);
}
#[test]
fn pressing_b_l_c_while_shape_is_selected_is_ignored() {
let mut state = a_state_over(A_BOX);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
state.select(Some(id1));
let b = Event::Key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
let l = Event::Key(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
let c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
assert_eq!(handle(&b, &mut state, Focus::Region), Reply::Ignored);
assert_eq!(handle(&l, &mut state, Focus::Region), Reply::Ignored);
assert_eq!(handle(&c, &mut state, Focus::Region), Reply::Ignored);
assert_eq!(state.selected(), Some(id1));
assert_eq!(state.session().diagram().shape_count(), 1);
}
#[test]
fn key_acceptance_matches_contextual_bar_in_idle_on_empty_diagram() {
const EMPTY: &str = r#"{ "next_id": 1, "shapes": [] }"#;
assert_offer_matches_acceptance(EMPTY, None, None);
}
#[test]
fn clicking_a_shape_when_pick_is_offered_selects_it() {
let mut state = a_state_over(A_BOX);
let bar = crate::status::command_line(&state);
assert!(bar.contains("[Click] Pick"));
assert!(state.selected().is_none());
let _ = paint(&mut state);
let at = screen(2, 1);
let reply = handle(&click(at.x, at.y), &mut state, Focus::Region);
assert_eq!(reply, Reply::Repaint);
assert_eq!(
state.selected(),
Some(ShapeId::new(NonZeroU16::new(1).unwrap()))
);
assert!(!crate::status::command_line(&state).contains("[Click] Pick"));
}
const TWO_BOXES_AND_A_CONNECTOR: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 8, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "connector", "id": 3,
"from": { "at": { "kind": "point", "x": 0, "y": 5 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 6, "y": 5 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" }
]
}"#;
const A_CONNECTOR_AND_A_BOX: &str = r#"{
"next_id": 3,
"shapes": [
{ "kind": "connector", "id": 1,
"from": { "at": { "kind": "point", "x": 2, "y": 1 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 10, "y": 1 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 10, "y": 3 },
"size": { "width": 4, "height": 3 }, "stroke": "light" }
]
}"#;
#[test]
fn clicking_a_candidate_in_attach_select_focuses_it_without_attaching() {
let mut state = a_state_over(TWO_BOXES_AND_A_CONNECTOR);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let id3 = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id3));
state.set_sub_mode(Some(SubMode::AttachSelect {
end: End::From,
target: id1,
}));
let _ = paint(&mut state);
let at = screen(8, 0);
let reply = handle(&click(at.x, at.y), &mut state, Focus::Region);
assert_eq!(reply, Reply::Repaint);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
assert!(matches!(
state.selected_shape(),
Some(monospace_diagram::Shape::Connector {
from: monospace_diagram::Endpoint {
at: monospace_diagram::EndpointAt::Absolute { .. },
..
},
..
})
));
}
#[test]
fn clicking_the_same_target_is_consumed_without_a_repaint() {
let mut state = a_state_over(TWO_BOXES_AND_A_CONNECTOR);
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let id3 = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id3));
state.set_sub_mode(Some(SubMode::AttachSelect {
end: End::From,
target: id2,
}));
let _ = paint(&mut state);
let at = screen(8, 0);
let reply = handle(&click(at.x, at.y), &mut state, Focus::Region);
assert_eq!(reply, Reply::Consumed);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
}
#[test]
fn clicking_a_connector_or_empty_cell_in_attach_select_leaves_the_focus_unchanged() {
let mut state = a_state_over(TWO_BOXES_AND_A_CONNECTOR);
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let id3 = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id3));
state.set_sub_mode(Some(SubMode::AttachSelect {
end: End::From,
target: id2,
}));
let _ = paint(&mut state);
let at_conn = screen(0, 5);
let reply_conn = handle(&click(at_conn.x, at_conn.y), &mut state, Focus::Region);
assert_eq!(reply_conn, Reply::Consumed);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
let at_empty = screen(15, 5);
let reply_empty = handle(&click(at_empty.x, at_empty.y), &mut state, Focus::Region);
assert_eq!(reply_empty, Reply::Consumed);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
}
#[test]
fn clicking_outside_the_interior_in_attach_select_is_ignored() {
let mut state = a_state_over(TWO_BOXES_AND_A_CONNECTOR);
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let id3 = ShapeId::new(NonZeroU16::new(3).unwrap());
state.select(Some(id3));
state.set_sub_mode(Some(SubMode::AttachSelect {
end: End::From,
target: id2,
}));
let _ = paint(&mut state);
let reply_border = handle(&click(0, 0), &mut state, Focus::Region);
assert_eq!(reply_border, Reply::Ignored);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
let reply_outside = handle(&click(25, 25), &mut state, Focus::Region);
assert_eq!(reply_outside, Reply::Ignored);
assert_eq!(
state.sub_mode(),
Some(SubMode::AttachSelect {
end: End::From,
target: id2,
})
);
assert_eq!(state.selected(), Some(id3));
}
#[test]
fn key_acceptance_matches_contextual_bar_in_attach_select() {
assert_offer_matches_acceptance(
A_CONNECTOR_AND_A_BOX,
Some(1),
Some(SubMode::AttachSelect {
end: End::From,
target: ShapeId::new(NonZeroU16::new(2).unwrap()),
}),
);
assert_offer_matches_acceptance(
A_CONNECTOR_AND_A_BOX,
Some(1),
Some(SubMode::AttachSelect {
end: End::To,
target: ShapeId::new(NonZeroU16::new(2).unwrap()),
}),
);
}
}