use rat_salsa::SalsaContext;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Constraint, Layout, Rect};
use ratatui_core::widgets::Widget;
use ratatui_widgets::clear::Clear;
use crate::context::AppGlobal;
use crate::state::AppState;
use crate::{diagram_widget, dialog, export, menu, open, save, status, text};
pub(crate) const BAR_HEIGHT: u16 = 1;
pub(crate) const STATUS_HEIGHT: u16 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Regions {
pub bar: Rect,
pub region: Rect,
pub status: Rect,
}
pub(crate) fn regions(area: Rect) -> Regions {
let [bar, region, status] = Layout::vertical([
Constraint::Length(BAR_HEIGHT),
Constraint::Fill(1),
Constraint::Length(STATUS_HEIGHT),
])
.areas(area);
Regions {
bar,
region,
status,
}
}
pub(crate) fn draw(area: Rect, buf: &mut Buffer, state: &mut AppState, global: &AppGlobal) {
let regions = regions(area);
Clear.render(area, buf);
let focused = state.chrome().is_bar();
menu::draw_bar(
regions.bar,
buf,
state.chrome_mut().menu_mut(),
&global.cfg.theme,
focused,
);
diagram_widget::draw(
regions.region,
buf,
state.diagram_mut(),
&global.catalog,
&global.cfg.theme,
);
status::draw(regions.status, buf, state.diagram(), &global.cfg.theme);
menu::draw_drop_down(area, buf, state.chrome_mut().menu_mut(), &global.cfg.theme);
dialog::draw(regions.region, buf, state.dialog_mut(), &global.cfg.theme);
let open_cursor = open::draw(regions.region, buf, state.open_mut(), &global.cfg.theme);
let save_cursor = save::draw(regions.region, buf, state.save_mut(), &global.cfg.theme);
let export_cursor = export::draw(regions.region, buf, state.export_mut(), &global.cfg.theme);
let text_cursor = text::draw(regions.region, buf, state.text_mut(), &global.cfg.theme);
global.set_screen_cursor(
text_cursor
.or(open_cursor)
.or(save_cursor)
.or(export_cursor),
);
}
#[cfg(test)]
pub(crate) fn cell_of(buffer: &Buffer, text: &str) -> (u16, u16) {
(buffer.area.top()..buffer.area.bottom())
.find_map(|line| {
let row: String = (buffer.area.left()..buffer.area.right())
.map(|column| buffer[(column, line)].symbol().to_string())
.collect();
row.find(text).map(|offset| {
let column =
buffer.area.left() + u16::try_from(offset).expect("an offset fits a frame");
(column, line)
})
})
.unwrap_or_else(|| panic!("{text} is not on the frame"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::context::AppGlobal;
use crate::diagram_state::{End, SubMode};
use crate::state::AppState;
use crate::status;
use crate::text::TextEditorState;
use crate::vocabulary::Reply;
use monospace_core::{Offset, ShapeId};
use monospace_diagram::Text;
use monospace_editing::Command;
use rat_theme4::palettes::dark::BASE16;
use rat_theme4::themes::create_dark;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Position;
use ratatui_core::style::Modifier;
use ratatui_crossterm::crossterm::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use std::num::NonZeroU16;
const WIDTH: u16 = 40;
const HEIGHT: u16 = 10;
const WHAT: &str = concat!(
"A picture of the screen, as the frame paints it. The three regions are counted under ",
"the picture: the bar across the top, the region the diagram goes into, and the status ",
"bar along the bottom. The count of the cells that are not blank is there because a ",
"picture alone cannot say how much of the screen is spoken for, and because a number ",
"that is derived from the frame cannot be wrong the way a hand-written expectation is.",
);
fn a_state() -> AppState {
AppState::new(crate::empty_session())
}
fn a_state_over(json: &str) -> AppState {
let diagram =
monospace_description::parse(json).expect("the test's description is readable");
AppState::new(monospace_editing::Session::new(diagram))
}
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_BOX_WITH_TEXT: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light",
"text": "hi" } ]
}"#;
const A_NARROW_BOX: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 2, "height": 2 }, "stroke": "light" } ]
}"#;
const A_BOX_AND_ITS_NEIGHBOR: &str = r#"{
"next_id": 3,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 6, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" } ]
}"#;
const AN_ASCII_BOX: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "ascii" } ]
}"#;
const A_BOX_IN_NO_TABLE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "mystery" } ]
}"#;
fn a_global() -> AppGlobal {
AppGlobal::new(Config::default(), monospace_glyph_sets::all())
}
fn paint(state: &mut AppState, global: &AppGlobal, width: u16, height: u16) -> Buffer {
let area = Rect::new(0, 0, width, height);
let mut buffer = Buffer::empty(area);
draw(area, &mut buffer, state, global);
buffer
}
fn row_of(buffer: &Buffer, line: u16) -> String {
(buffer.area.left()..buffer.area.right())
.map(|column| buffer[(column, line)].symbol().to_string())
.collect()
}
fn picture(buffer: &Buffer) -> String {
(buffer.area.top()..buffer.area.bottom())
.map(|line| row_of(buffer, line).trim_end().to_string())
.collect::<Vec<String>>()
.join("\n")
}
fn drop_down_picture(buffer: &Buffer) -> String {
let top = BAR_HEIGHT;
let row: Vec<char> = row_of(buffer, top).chars().collect();
let mut left = None;
for (column, glyph) in row.iter().enumerate() {
if *glyph == '┌' {
left = Some(column);
}
}
let left = left.expect("the drop-down has no upper-left corner");
let right = row
.iter()
.skip(left)
.position(|glyph| *glyph == '┐')
.map(|offset| left + offset)
.expect("the drop-down has no upper-right corner");
let mut lines = Vec::new();
for line in top..buffer.area.bottom() {
let text: String = row_of(buffer, line)
.chars()
.skip(left)
.take(right - left + 1)
.collect();
let last = text.starts_with('└');
lines.push(text);
if last {
break;
}
}
lines.join("\n")
}
fn written(buffer: &Buffer) -> usize {
(buffer.area.top()..buffer.area.bottom())
.flat_map(|line| {
(buffer.area.left()..buffer.area.right()).map(move |column| (line, column))
})
.filter(|(line, column)| buffer[(*column, *line)].symbol() != " ")
.count()
}
fn counts(buffer: &Buffer, regions: Regions) -> String {
let total = usize::from(buffer.area.width) * usize::from(buffer.area.height);
format!(
"the bar is {} {}, the region is {} {} and the status is {} {}\n\
{} of {total} cells are not blank",
regions.bar.height,
rows(regions.bar.height),
regions.region.height,
rows(regions.region.height),
regions.status.height,
rows(regions.status.height),
written(buffer),
)
}
fn rows(count: u16) -> &'static str {
if count == 1 { "row" } else { "rows" }
}
fn snap(name: &str, body: &str) {
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/snapshots/contract"
));
settings.set_description(WHAT);
settings.bind(|| insta::assert_snapshot!(name, body));
}
fn open_the_drop_down(state: &mut AppState, global: &AppGlobal, navigation: char) {
let _ = paint(state, global, WIDTH, HEIGHT);
state.chrome_mut().move_to_bar();
let event = Event::Key(KeyEvent::new(KeyCode::Char(navigation), KeyModifiers::NONE));
let _ = crate::menu::handle(&event, state.chrome_mut());
assert!(
state.chrome().popup_active(),
"the navigation character did not open the drop-down"
);
}
fn described(buffer: &Buffer, regions: Regions) -> String {
format!("{}\n{}", picture(buffer), counts(buffer, regions))
}
fn reversed(buffer: &Buffer, column: u16, line: u16) -> bool {
buffer[(column, line)]
.style()
.add_modifier
.contains(Modifier::REVERSED)
}
fn inverted(buffer: &Buffer) -> Vec<(u16, u16)> {
(buffer.area.top()..buffer.area.bottom())
.flat_map(|line| {
(buffer.area.left()..buffer.area.right()).map(move |column| (column, line))
})
.filter(|(column, line)| reversed(buffer, *column, *line))
.collect()
}
fn cells_of_the_box() -> Vec<(u16, u16)> {
vec![
(3, 3),
(4, 3),
(5, 3),
(6, 3),
(3, 4),
(6, 4),
(3, 5),
(4, 5),
(5, 5),
(6, 5),
]
}
fn click(state: &mut AppState, column: u16, line: u16) {
let event = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row: line,
modifiers: KeyModifiers::NONE,
});
let target = state.target_at(Position::new(column, line));
let leaf = state.chrome().leaf();
let outcome = crate::diagram_widget::handle(&event, state.diagram_mut(), leaf, target);
assert!(
matches!(outcome, Reply::Repaint),
"({column}, {line}) selected nothing: {outcome:?}"
);
}
#[test]
fn the_bar_is_at_the_top_and_the_status_bar_is_at_the_bottom() {
let buffer = paint(&mut a_state(), &a_global(), WIDTH, HEIGHT);
let regions = regions(Rect::new(0, 0, WIDTH, HEIGHT));
let bar = row_of(&buffer, 0);
let region = row_of(&buffer, 1);
let status_bar = row_of(&buffer, regions.status.top());
let command_bar = row_of(&buffer, regions.status.top() + 1);
assert!(bar.contains("File"), "the bar has no File on it: {bar:?}");
assert!(
region.starts_with('┌'),
"the region does not start under the bar: {region:?}"
);
assert!(
status_bar.contains(status::NAME),
"the status bar says nothing: {status_bar:?}"
);
assert!(
command_bar.starts_with("CREATE: [b] Box [l] Line [c] Connector"),
"the command bar offers shape creation when diagram has no shapes: {command_bar:?}"
);
}
#[test]
fn the_region_between_them_takes_the_rest_of_the_screen() {
for height in [3, 10, 24] {
let regions = regions(Rect::new(0, 0, WIDTH, height));
assert_eq!(regions.bar.height, BAR_HEIGHT, "{height} rows");
assert_eq!(regions.status.height, STATUS_HEIGHT, "{height} rows");
assert_eq!(
regions.region.height,
height - BAR_HEIGHT - STATUS_HEIGHT,
"{height} rows"
);
assert_eq!(regions.region.top(), BAR_HEIGHT, "{height} rows");
assert_eq!(
regions.region.bottom(),
height - STATUS_HEIGHT,
"{height} rows"
);
}
}
#[test]
fn the_interior_is_the_window_the_diagram_is_drawn_into() {
let mut state = a_state_over(A_BOX);
let buffer = paint(&mut state, &a_global(), WIDTH, HEIGHT);
assert_eq!(
buffer[(3, 3)].symbol(),
"┌",
"the box's top-left is not at the interior's (2, 1)"
);
assert_eq!(
buffer[(6, 3)].symbol(),
"┐",
"the box is not four cells wide from the interior's corner"
);
assert_eq!(
buffer[(3, 5)].symbol(),
"└",
"the box is not three cells tall from the interior's corner"
);
assert_eq!(
buffer[(1, 2)].symbol(),
" ",
"the interior's own corner holds something"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the box does not own the cell it drew"
);
}
#[test]
fn the_diagram_is_drawn_again_on_every_frame() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
assert!(
state.diagram().owner(Offset { x: 2, y: 2 }).is_some(),
"the first frame did not draw the box"
);
let _ = paint(&mut state, &global, WIDTH, 6);
assert!(
state.diagram().owner(Offset { x: 2, y: 2 }).is_none(),
"the drawing of the first frame survived the second"
);
}
#[test]
fn a_cell_no_shape_wrote_is_a_space() {
let buffer = paint(&mut a_state_over(A_BOX), &a_global(), WIDTH, HEIGHT);
assert_eq!(
buffer[(1, 2)].symbol(),
" ",
"the interior's corner, which no shape wrote, is not a space"
);
}
#[test]
fn a_glyph_the_catalog_does_not_know_is_a_space() {
let buffer = paint(
&mut a_state_over(A_BOX_IN_NO_TABLE),
&a_global(),
WIDTH,
HEIGHT,
);
for line in 2..5 {
for column in 1..5 {
assert_eq!(
buffer[(column, line)].symbol(),
" ",
"({column}, {line}) is not a space"
);
}
}
}
#[test]
fn the_owner_survives_the_copy() {
let mut state = a_state_over(A_BOX);
let buffer = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let diagram = state.diagram();
let top_left = Offset { x: 2, y: 1 };
let top_right = Offset { x: 5, y: 1 };
assert!(
diagram.owner(top_left).is_some(),
"the copy took the record with it"
);
assert_eq!(
diagram.owner(top_left),
diagram.owner(top_right),
"two cells of the same box name two different shapes"
);
assert_eq!(buffer[(3, 3)].symbol(), "┌", "the copy drew no box");
assert_eq!(buffer[(6, 3)].symbol(), "┐", "the copy drew no box");
}
#[test]
fn the_owner_is_asked_with_the_click_offset_and_nothing_added() {
let mut state = a_state_over(A_NARROW_BOX);
let _ = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let diagram = state.diagram();
assert!(
diagram.owner(Offset { x: 2, y: 1 }).is_some(),
"the click's own offset is not the one asked"
);
assert!(
diagram.owner(Offset { x: 3, y: 3 }).is_none(),
"the interior's corner was added to the click"
);
}
#[test]
fn the_buffer_of_the_last_frame_answers_a_click() {
let mut state = a_state_over(A_BOX);
let global = a_global();
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the empty drawing answered a click"
);
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
let asked = state.diagram().owner(Offset { x: 2, y: 1 });
assert!(asked.is_some(), "the frame's drawing does not answer");
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
assert_eq!(
state.diagram().owner(Offset { x: 2, y: 1 }),
asked,
"the second frame's drawing does not answer the same click"
);
}
#[test]
fn a_click_changes_no_diagram() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let before = paint(&mut state, &global, WIDTH, HEIGHT);
let asked = state.diagram().owner(Offset { x: 2, y: 1 });
assert!(asked.is_some(), "the box is not in the drawing");
click(&mut state, 3, 3);
let after = paint(&mut state, &global, WIDTH, HEIGHT);
assert_eq!(
state.diagram().owner(Offset { x: 2, y: 1 }),
asked,
"the click changed which shape the drawing records"
);
let region = regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
for line in region.top()..region.bottom() {
assert_eq!(
row_of(&before, line),
row_of(&after, line),
"the click changed row {line} in the region"
);
}
assert!(
!inverted(&after).is_empty(),
"the click selected nothing, so the frame proves nothing"
);
}
#[test]
fn the_selected_shapes_cells_are_inverted_and_no_others() {
let mut state = a_state_over(A_BOX_AND_ITS_NEIGHBOR);
let global = a_global();
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
click(&mut state, 3, 3);
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
assert_eq!(
inverted(&buffer),
cells_of_the_box(),
"the cells the frame inverts are not the selected box's"
);
assert!(
!reversed(&buffer, 7, 3),
"the neighboring box's border is inverted"
);
}
const A_BOX_A_CONNECTOR_AND_A_NEIGHBOR: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "connector", "id": 2,
"from": { "at": { "kind": "point", "x": 8, "y": 2 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 12, "y": 2 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" },
{ "kind": "box", "id": 3, "at": { "x": 16, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" }
]
}"#;
#[test]
fn the_attach_picker_inverts_the_target_and_the_selected_connector() {
let mut state = a_state_over(A_BOX_A_CONNECTOR_AND_A_NEIGHBOR);
let global = a_global();
let id1 = ShapeId::new(NonZeroU16::new(1).expect("1 is non-zero"));
let id2 = ShapeId::new(NonZeroU16::new(2).expect("2 is non-zero"));
state.diagram_mut().select(Some(id2));
let conn_buffer = paint(&mut state, &global, WIDTH, HEIGHT);
let conn_cells = inverted(&conn_buffer);
assert!(
!conn_cells.is_empty(),
"connector cells should not be empty"
);
state
.diagram_mut()
.set_sub_mode(Some(SubMode::AttachSelect {
end: End::From,
target: id1,
}));
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
let mut expected = cells_of_the_box();
expected.extend(conn_cells);
expected.sort_by_key(|&(x, y)| (y, x));
expected.dedup();
assert_eq!(
inverted(&buffer),
expected,
"the attach picker inverts exactly the target box and the selected connector"
);
assert!(
!reversed(&buffer, 17, 3),
"the neighboring box's border is inverted"
);
}
#[test]
fn a_selection_the_drawing_does_not_record_inverts_nothing() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
let unrecorded = ShapeId::new(NonZeroU16::new(9).expect("9 is not zero"));
assert!(
state.diagram_mut().select(Some(unrecorded)),
"the selection did not move"
);
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
assert!(
inverted(&buffer).is_empty(),
"an identity no cell records inverted cells"
);
}
#[test]
fn a_selected_shape_with_no_cell_in_the_window_is_still_selected() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
click(&mut state, 3, 3);
let empty = paint(&mut state, &global, WIDTH, 4);
assert!(
inverted(&empty).is_empty(),
"a cell of a shape the window does not hold is inverted"
);
assert!(
state.diagram().selected().is_some(),
"the selection was dropped when the window stopped holding the shape"
);
let held_again = paint(&mut state, &global, WIDTH, HEIGHT);
assert_eq!(
inverted(&held_again),
cells_of_the_box(),
"the frame that holds the box again does not invert it"
);
}
#[test]
fn the_selection_survives_a_resize() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
click(&mut state, 3, 3);
let clipped = paint(&mut state, &global, WIDTH, 8);
let rows_the_window_holds: Vec<(u16, u16)> = cells_of_the_box()
.into_iter()
.filter(|(_, line)| *line < 5)
.collect();
assert_eq!(
inverted(&clipped),
rows_the_window_holds,
"the resize inverted cells the window does not hold, or dropped the cells it does"
);
let restored = paint(&mut state, &global, WIDTH, HEIGHT);
assert_eq!(
inverted(&restored),
cells_of_the_box(),
"the selection did not survive the resize"
);
}
#[test]
fn the_dialog_is_drawn_over_the_drop_down() {
let mut state = a_state();
let global = a_global();
open_the_drop_down(&mut state, &global, 'f');
state.dialog_mut().show(crate::about::open());
let frame = picture(&paint(&mut state, &global, WIDTH, HEIGHT));
assert!(
frame.contains(&format!("About {}", status::NAME)),
"the dialog is not on the frame: {frame:?}"
);
assert!(
!frame.contains("Quit"),
"the drop-down is drawn over the dialog: {frame:?}"
);
}
#[test]
fn the_palette_is_the_dark_base16_theme() {
let by_default = paint(&mut a_state(), &a_global(), WIDTH, HEIGHT);
let dark = AppGlobal::new(
Config {
theme: create_dark(BASE16),
},
monospace_glyph_sets::all(),
);
let by_the_theme = paint(&mut a_state(), &dark, WIDTH, HEIGHT);
for line in 0..HEIGHT {
for column in 0..WIDTH {
assert_eq!(
by_default[(column, line)].symbol(),
by_the_theme[(column, line)].symbol(),
"({column}, {line}) is not what the dark BASE16 theme paints"
);
assert_eq!(
by_default[(column, line)].style(),
by_the_theme[(column, line)].style(),
"({column}, {line}) is painted in another palette"
);
}
}
}
#[test]
fn the_region_is_drawn_the_same_with_and_without_the_keyboard() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let region = regions(area).region;
state.chrome_mut().move_to_region();
let without = paint(&mut state, &global, WIDTH, HEIGHT);
state.chrome_mut().move_to_bar();
let with = paint(&mut state, &global, WIDTH, HEIGHT);
for line in region.top()..region.bottom() {
for column in region.left()..region.right() {
assert_eq!(
with[(column, line)].symbol(),
without[(column, line)].symbol(),
"({column}, {line}) changed with the keyboard"
);
assert_eq!(
with[(column, line)].style(),
without[(column, line)].style(),
"({column}, {line}) was painted differently with the keyboard"
);
}
}
}
#[test]
fn an_ascii_box_is_drawn_with_the_ascii_table() {
let buffer = paint(&mut a_state_over(AN_ASCII_BOX), &a_global(), WIDTH, HEIGHT);
assert!(
row_of(&buffer, 2).starts_with("│+--+"),
"the box's top is not in the ASCII table: {:?}",
row_of(&buffer, 2)
);
assert!(
row_of(&buffer, 3).starts_with("│| |"),
"the box's sides are not in the ASCII table: {:?}",
row_of(&buffer, 3)
);
assert!(
row_of(&buffer, 4).starts_with("│+--+"),
"the box's bottom is not in the ASCII table: {:?}",
row_of(&buffer, 4)
);
}
#[test]
fn an_empty_diagram_draws_nothing() {
let buffer = paint(&mut a_state(), &a_global(), WIDTH, HEIGHT);
let region = regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
for line in region.top() + 1..region.bottom() - 1 {
for column in region.left() + 1..region.right() - 1 {
assert_eq!(
buffer[(column, line)].symbol(),
" ",
"({column}, {line}) is not blank"
);
}
}
assert!(
row_of(&buffer, region.top()).starts_with('┌'),
"the frame went with the diagram"
);
}
#[test]
fn a_region_with_no_interior_draws_nothing() {
let mut state = a_state_over(A_BOX);
let buffer = paint(&mut state, &a_global(), WIDTH, 4);
assert_eq!(
row_of(&buffer, 1),
" ".repeat(usize::from(WIDTH)),
"a region with no interior drew something"
);
assert!(
state.diagram().owner(Offset { x: 0, y: 0 }).is_none(),
"a window with no cells owns one"
);
}
#[test]
fn the_status_bar_says_how_far_the_drawing_reaches() {
let mut state = a_state_over(A_BOX);
let whole = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let clipped = paint(&mut state, &a_global(), WIDTH, 6);
assert!(
row_of(&whole, HEIGHT - 2).contains("4×3 at 2,1"),
"the bar does not say where the box of A_BOX reaches: {:?}",
row_of(&whole, HEIGHT - 2)
);
assert!(
row_of(&clipped, 4).contains("4×3 at 2,1"),
"a window too small to hold the box changed how far it reaches: {:?}",
row_of(&clipped, 4)
);
}
#[test]
fn a_blank_diagram_is_drawn_when_no_path_is_given() {
let session = crate::start(None).expect("startup with no path succeeds");
let buffer = paint(&mut AppState::new(session), &a_global(), 60, HEIGHT);
assert!(
row_of(&buffer, HEIGHT - 2).contains("nothing drawn"),
"the status bar does not say nothing drawn for blank startup"
);
}
#[test]
fn the_description_at_the_path_is_drawn() {
let path =
std::env::temp_dir().join(format!("monospace-app-frame-{}.json", std::process::id()));
std::fs::write(&path, A_BOX).expect("the temp description should be writable");
let named = path.to_str().expect("the temp path is UTF-8");
let session = crate::start(Some(named)).expect("the description at the path is readable");
let buffer = paint(&mut AppState::new(session), &a_global(), WIDTH, HEIGHT);
let _ = std::fs::remove_file(&path);
assert_eq!(
buffer[(3, 3)].symbol(),
"┌",
"the description at the path is not drawn where its box says"
);
}
#[test]
fn the_demonstrations_picture_keeps_its_cells() {
let session = crate::demo().expect("the shipped demonstration is readable");
let mut state = AppState::new(session);
let buffer = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let region = regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
let catalog = monospace_glyph_sets::all();
for line in region.top() + 1..region.bottom() - 1 {
for column in region.left() + 1..region.right() - 1 {
let at = Offset {
x: column - region.left() - 1,
y: line - region.top() - 1,
};
let drawn = state.diagram().glyph(at, &catalog).unwrap_or(" ");
assert_eq!(
buffer[(column, line)].symbol(),
drawn,
"({column}, {line}) is not the cell the drawing holds there"
);
}
}
let first = row_of(&buffer, region.top() + 1);
assert!(
first.starts_with("│┌──┐"),
"the demonstration's first row moved: {first:?}"
);
assert!(
first.contains("+--+"),
"the demonstration's ASCII box moved: {first:?}"
);
}
#[test]
fn the_screen_with_the_demo() {
let session = crate::demo().expect("the shipped demonstration is readable");
let mut state = AppState::new(session);
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let buffer = paint(&mut state, &a_global(), WIDTH, HEIGHT);
snap(
"the_screen_with_the_demo",
&format!(
"the screen with the demonstration, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
}
#[test]
fn the_bars_row_is_drawn_on_every_frame() {
let mut state = a_state();
let global = a_global();
let at_rest = row_of(&paint(&mut state, &global, WIDTH, HEIGHT), 0);
state.chrome_mut().menu_mut().bar.select(Some(0));
state.chrome_mut().menu_mut().set_popup_active(true);
let with_drop_down = row_of(&paint(&mut state, &global, WIDTH, HEIGHT), 0);
assert!(
at_rest.contains("File"),
"the bar has no File on it: {at_rest:?}"
);
assert_eq!(at_rest, with_drop_down, "the drop-down took the bar's row");
}
#[test]
fn a_region_that_stops_being_drawn_does_not_keep_what_was_last_in_it() {
let mut state = a_state();
let global = a_global();
let mut buffer = Buffer::empty(Rect::new(0, 0, WIDTH, HEIGHT));
draw(
Rect::new(0, 0, WIDTH, HEIGHT),
&mut buffer,
&mut state,
&global,
);
assert!(
row_of(&buffer, 1).starts_with('┌'),
"the region is not a box: {:?}",
row_of(&buffer, 1)
);
draw(Rect::new(0, 0, WIDTH, 4), &mut buffer, &mut state, &global);
assert_eq!(
row_of(&buffer, 1),
" ".repeat(usize::from(WIDTH)),
"the region kept what was last in it"
);
}
#[test]
fn a_drop_down_is_not_erased_by_the_region_being_painted_after_it() {
let mut state = a_state();
let global = a_global();
open_the_drop_down(&mut state, &global, 'f');
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
let frame = picture(&buffer);
assert!(
frame.contains("Quit"),
"the drop-down shows nothing: {frame:?}"
);
assert!(frame.contains('┐'), "the drop-down has no frame: {frame:?}");
}
#[test]
fn a_drop_down_leaves_the_region_byte_identical_when_it_closes() {
let mut state = a_state_over(A_BOX);
let global = a_global();
let before = paint(&mut state, &global, WIDTH, HEIGHT);
open_the_drop_down(&mut state, &global, 'f');
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
state.chrome_mut().menu_mut().set_popup_active(false);
let after = paint(&mut state, &global, WIDTH, HEIGHT);
for line in 0..HEIGHT {
assert_eq!(
row_of(&before, line),
row_of(&after, line),
"row {line} is not what it was"
);
}
}
#[test]
fn the_edit_menu_offers_undo_and_redo_at_every_position() {
let mut state = a_state_over(A_BOX);
let global = a_global();
open_the_drop_down(&mut state, &global, 'e');
let fresh = drop_down_picture(&paint(&mut state, &global, WIDTH, HEIGHT));
state.chrome_mut().menu_mut().set_popup_active(false);
let shape = ShapeId::new(NonZeroU16::new(1).expect("1 is not zero"));
state.diagram_mut().perform(Command::Remove { id: shape });
let _ = paint(&mut state, &global, WIDTH, HEIGHT);
open_the_drop_down(&mut state, &global, 'e');
let after_the_delete = drop_down_picture(&paint(&mut state, &global, WIDTH, HEIGHT));
assert!(
fresh.contains("Undo"),
"the drop-down shows no undo: {fresh:?}"
);
assert!(
fresh.contains("Redo"),
"the drop-down shows no redo: {fresh:?}"
);
assert_eq!(
fresh, after_the_delete,
"the Edit menu moves with the history"
);
}
#[test]
fn the_screen_at_rest() {
let mut state = a_state();
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
snap(
"the_screen_at_rest",
&format!(
"the screen at rest, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
}
#[test]
fn the_drop_down_open_over_the_region() {
let mut state = a_state();
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
open_the_drop_down(&mut state, &global, 'f');
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
snap(
"the_drop_down_open_over_the_region",
&format!(
"the drop-down open over the region, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
}
#[test]
fn the_edit_drop_down() {
let mut state = a_state();
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
open_the_drop_down(&mut state, &global, 'e');
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
snap(
"the_edit_drop_down",
&format!(
"the Edit drop-down open over the region, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
}
#[test]
fn the_status_bar_below_the_region() {
let mut state = a_state();
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
let regions = regions(area);
let status_bar = row_of(&buffer, regions.status.top());
let command_bar = row_of(&buffer, regions.status.top() + 1);
assert!(
regions.status.top() == regions.region.bottom(),
"the status bar is not below the region"
);
assert!(
status_bar.contains(status::NAME),
"the status bar says nothing: {status_bar:?}"
);
assert!(
command_bar.starts_with("CREATE: [b] Box [l] Line [c] Connector"),
"the command bar offers shape creation when diagram has no shapes: {command_bar:?}"
);
snap(
"the_status_bar_below_the_region",
&format!(
"the status bar below the region, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions)
),
);
}
#[test]
fn the_status_bar_is_unchanged_while_editing() {
let mut state = a_state_over(A_BOX);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.diagram_mut().select(Some(id));
let before = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let status = regions(Rect::new(0, 0, WIDTH, HEIGHT)).status;
let before_rows: Vec<String> = (status.top()..status.bottom())
.map(|line| row_of(&before, line))
.collect();
let text = Text::new("hi").unwrap();
state.text_mut().show(TextEditorState::new(id, Some(&text)));
let after = paint(&mut state, &a_global(), WIDTH, HEIGHT);
let after_rows: Vec<String> = (status.top()..status.bottom())
.map(|line| row_of(&after, line))
.collect();
assert_eq!(
before_rows, after_rows,
"opening the editor changed the status bar"
);
}
#[test]
fn the_text_editor_over_a_box() {
let mut state = a_state_over(A_BOX_WITH_TEXT);
let global = a_global();
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.diagram_mut().select(Some(id));
let text = Text::new("hi").unwrap();
state.text_mut().show(TextEditorState::new(id, Some(&text)));
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
snap(
"a_box_with_the_text_editor_over_it",
&format!(
"a box with the text editor over it, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
state.text_mut().close();
let buffer = paint(&mut state, &global, WIDTH, HEIGHT);
snap(
"the_same_box_after_the_editor_closes",
&format!(
"the same box after the editor closes, {WIDTH}x{HEIGHT}\n{}",
described(&buffer, regions(area))
),
);
}
}