/* * meli * * Copyright 2017-2018 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ //! Terminal grid cells, keys, colors, etc. use std::io::{BufWriter, Write}; use melib::{log, uuid}; use termion::{clear, cursor, raw::IntoRawMode, screen::AlternateScreen}; use crate::{ terminal::{ cells::CellBuffer, position::*, BracketModeEnd, BracketModeStart, Cell, Color, DisableMouse, DisableSGRMouse, EnableMouse, EnableSGRMouse, RestoreWindowTitleIconFromStack, SaveWindowTitleIconToStack, }, Attr, Context, }; pub type StateStdout = termion::screen::AlternateScreen< termion::raw::RawTerminal>>, >; type DrawHorizontalSegmentFn = fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (); #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] #[repr(transparent)] pub struct ScreenGeneration((u64, u64)); impl ScreenGeneration { pub const NIL: ScreenGeneration = Self((0, 0)); #[inline] pub fn next(self) -> Self { Self(uuid::Uuid::new_v4().as_u64_pair()) } } impl Default for ScreenGeneration { fn default() -> Self { Self::NIL } } #[derive(Clone, Copy, Debug)] pub struct Virtual; pub struct Tty { stdout: Option, mouse: bool, draw_horizontal_segment_fn: DrawHorizontalSegmentFn, } impl Tty { #[inline] pub fn stdout_mut(&mut self) -> Option<&mut StateStdout> { self.stdout.as_mut() } #[inline] pub fn draw_fn(&self) -> DrawHorizontalSegmentFn { self.draw_horizontal_segment_fn } #[inline] pub fn mouse(&self) -> bool { self.mouse } #[inline] pub fn set_mouse(&mut self, mouse: bool) -> &mut Self { self.mouse = mouse; self } #[inline] pub fn set_draw_fn( &mut self, draw_horizontal_segment_fn: DrawHorizontalSegmentFn, ) -> &mut Self { self.draw_horizontal_segment_fn = draw_horizontal_segment_fn; self } } mod private { pub trait Sealed {} } impl private::Sealed for Virtual {} impl private::Sealed for Tty {} pub struct Screen { cols: usize, rows: usize, grid: CellBuffer, overlay_grid: CellBuffer, display: Display, generation: ScreenGeneration, } impl std::fmt::Debug for Screen { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct(stringify!(Screen)) .field("cols", &self.cols) .field("rows", &self.rows) .field("grid", &self.grid) .field("overlay_grid", &self.overlay_grid) .field("generation", &self.generation) .finish() } } impl Screen { #[inline] pub fn init(display: D) -> Self { let area = Area { offset: (0, 0), upper_left: (0, 0), bottom_right: (0, 0), empty: true, canvas_cols: 0, canvas_rows: 0, generation: ScreenGeneration::NIL, }; Self { cols: 0, rows: 0, grid: CellBuffer::nil(area), overlay_grid: CellBuffer::nil(area), display, generation: ScreenGeneration::NIL, } } #[inline] pub fn with_cols_and_rows(mut self, cols: usize, rows: usize) -> Self { self.generation = self.generation.next(); self.cols = cols; self.rows = rows; Self { cols, rows, grid: CellBuffer::new(Cell::with_char(' '), self.area()), overlay_grid: CellBuffer::new(Cell::with_char(' '), self.area()), ..self } } pub const fn area(&self) -> Area { let upper_left = (0, 0); let bottom_right = (self.cols.saturating_sub(1), self.rows.saturating_sub(1)); Area { offset: upper_left, upper_left, bottom_right, empty: matches!((self.cols, self.rows), (0, 0)), canvas_cols: self.cols, canvas_rows: self.rows, generation: self.generation, } } #[inline] pub fn grid(&self) -> &CellBuffer { &self.grid } #[inline] pub fn grid_mut(&mut self) -> &mut CellBuffer { &mut self.grid } #[inline] pub fn overlay_grid(&self) -> &CellBuffer { &self.overlay_grid } #[inline] pub fn overlay_grid_mut(&mut self) -> &mut CellBuffer { &mut self.overlay_grid } #[inline] pub fn grid_and_overlay_grid_mut(&mut self) -> (&mut CellBuffer, &mut CellBuffer) { (&mut self.grid, &mut self.overlay_grid) } #[inline(always)] pub const fn generation(&self) -> ScreenGeneration { self.generation } #[inline] pub const fn cols(&self) -> usize { self.cols } #[inline] pub const fn rows(&self) -> usize { self.rows } } impl Clone for Screen { fn clone(&self) -> Self { Self { grid: self.grid.clone(), overlay_grid: self.overlay_grid.clone(), ..*self } } } impl Screen { #[inline] pub fn new() -> Self { Self::init(Tty { stdout: None, mouse: false, draw_horizontal_segment_fn: Screen::draw_horizontal_segment, }) } #[inline] pub fn with_tty(self, display: Tty) -> Self { Self { display, ..self } } #[inline] pub fn tty(&self) -> &Tty { &self.display } #[inline] pub fn tty_mut(&mut self) -> &mut Tty { &mut self.display } #[inline] pub fn draw(&mut self, x_start: usize, x_end: usize, y: usize) { let Some(stdout) = self.display.stdout.as_mut() else { return; }; (self.display.draw_horizontal_segment_fn)(&mut self.grid, stdout, x_start, x_end, y); } /// On `SIGWNICH` the `State` redraws itself according to the new /// terminal size. pub fn update_size(&mut self) { let termsize = termion::terminal_size().ok(); let termcols = termsize.map(|(w, _)| w); let termrows = termsize.map(|(_, h)| h); if termcols.unwrap_or(72) as usize != self.cols || termrows.unwrap_or(120) as usize != self.rows { log::trace!( "Size updated, from ({}, {}) -> ({:?}, {:?})", self.cols, self.rows, termcols, termrows ); } let cols = termcols.unwrap_or(72) as usize; let rows = termrows.unwrap_or(120) as usize; if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) { self.generation = self.generation.next(); self.cols = cols; self.rows = rows; self.grid.area = self.area(); self.overlay_grid.area = self.area(); } else { log::warn!("Terminal size too big: ({} cols, {} rows)", cols, rows); } } /// Switch back to the terminal's main screen (The command line the user /// sees before opening the application) pub fn switch_to_main_screen(&mut self) { let Some(stdout) = self.display.stdout.as_mut() else { return; }; let mouse = self.display.mouse; write!( stdout, "{}{}{}{}{disable_sgr_mouse}{disable_mouse}", termion::screen::ToMainScreen, cursor::Show, RestoreWindowTitleIconFromStack, BracketModeEnd, disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" }, disable_mouse = if mouse { DisableMouse.as_ref() } else { "" }, ) .unwrap(); self.flush(); self.display.stdout = None; } pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) { let mut stdout = BufWriter::with_capacity(240 * 80, Box::new(std::io::stdout()) as _); write!( &mut stdout, "{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}", termion::screen::ToAlternateScreen, cursor::Hide, clear::All, cursor::Goto(1, 1), BracketModeStart, save_title_to_stack = SaveWindowTitleIconToStack, window_title = if let Some(ref title) = context.settings.terminal.window_title { format!("\x1b]2;{}\x07", title) } else { String::new() }, enable_mouse = if self.display.mouse { EnableMouse.as_ref() } else { "" }, enable_sgr_mouse = if self.display.mouse { EnableSGRMouse.as_ref() } else { "" }, ) .unwrap(); self.display.stdout = Some(AlternateScreen::from(stdout.into_raw_mode().unwrap())); self.flush(); } #[inline] pub fn flush(&mut self) { if let Some(stdout) = self.display.stdout.as_mut() { stdout.flush().unwrap(); } } /// Draw only a specific `area` on the screen. pub fn draw_horizontal_segment( grid: &mut CellBuffer, stdout: &mut StateStdout, x_start: usize, x_end: usize, y: usize, ) { write!( stdout, "{}", cursor::Goto(x_start as u16 + 1, (y + 1) as u16) ) .unwrap(); let mut current_fg = Color::Default; let mut current_bg = Color::Default; let mut current_attrs = Attr::DEFAULT; write!(stdout, "\x1B[m").unwrap(); for x in x_start..=x_end { let c = &grid[(x, y)]; if c.attrs() != current_attrs { c.attrs().write(current_attrs, stdout).unwrap(); current_attrs = c.attrs(); } if c.bg() != current_bg { c.bg().write_bg(stdout).unwrap(); current_bg = c.bg(); } if c.fg() != current_fg { c.fg().write_fg(stdout).unwrap(); current_fg = c.fg(); } if !c.empty() { write!(stdout, "{}", c.ch()).unwrap(); } } } pub fn draw_horizontal_segment_no_color( grid: &mut CellBuffer, stdout: &mut StateStdout, x_start: usize, x_end: usize, y: usize, ) { write!( stdout, "{}", cursor::Goto(x_start as u16 + 1, (y + 1) as u16) ) .unwrap(); let mut current_attrs = Attr::DEFAULT; write!(stdout, "\x1B[m").unwrap(); for x in x_start..=x_end { let c = &grid[(x, y)]; if c.attrs() != current_attrs { c.attrs().write(current_attrs, stdout).unwrap(); current_attrs = c.attrs(); } if !c.empty() { write!(stdout, "{}", c.ch()).unwrap(); } } } } impl Default for Screen { fn default() -> Self { Self::new() } } impl Screen { #[inline] pub fn new() -> Self { Self::init(Virtual) } #[must_use] pub fn resize(&mut self, cols: usize, rows: usize) -> bool { if self.grid.resize(cols, rows, None) && self.overlay_grid.resize(cols, rows, None) { self.generation = self.generation.next(); self.cols = cols; self.rows = rows; self.grid.area = self.area(); self.overlay_grid.area = self.area(); return true; } false } #[must_use] pub fn resize_with_context(&mut self, cols: usize, rows: usize, context: &Context) -> bool { if self.grid.resize_with_context(cols, rows, context) && self.overlay_grid.resize_with_context(cols, rows, context) { self.generation = self.generation.next(); self.cols = cols; self.rows = rows; self.grid.area = self.area(); self.overlay_grid.area = self.area(); return true; } false } } /// An `Area` consists of two points: the upper left and bottom right corners. #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct Area { offset: Pos, upper_left: Pos, bottom_right: Pos, empty: bool, canvas_cols: usize, canvas_rows: usize, generation: ScreenGeneration, } impl std::fmt::Debug for Area { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct(stringify!(Area)) .field("width", &self.width()) .field("height", &self.height()) .field("offset", &self.offset) .field("upper_left", &self.upper_left) .field("bottom_right", &self.bottom_right) .field("empty flag", &self.empty) .field("is_empty", &self.is_empty()) .field("canvas_cols", &self.canvas_cols) .field("canvas_rows", &self.canvas_rows) .field("generation", &self.generation) .finish() } } impl From<&Screen> for Area { fn from(sc: &Screen) -> Self { sc.area() } } impl Area { #[inline] pub fn height(&self) -> usize { if self.is_empty() { return 0; } get_y(self.bottom_right).saturating_sub(get_y(self.upper_left)) + 1 } #[inline] pub fn width(&self) -> usize { if self.is_empty() { return 0; } get_x(self.bottom_right).saturating_sub(get_x(self.upper_left)) + 1 } #[inline] pub fn size(&self) -> (usize, usize) { (self.width(), self.height()) } /// Get `n`th row of `area` or its last one. #[inline] pub fn nth_row(&self, n: usize) -> Self { let Self { offset, upper_left, bottom_right, empty, canvas_cols, canvas_rows, generation, } = *self; let (_, max_y) = bottom_right; let n = std::cmp::min(n, self.height()); if self.is_empty() || max_y < (get_y(upper_left) + n) { return self.into_empty(); } let y = std::cmp::min(max_y, get_y(upper_left) + n); Self { offset: pos_inc(offset, (0, n)), upper_left: set_y(upper_left, y), bottom_right: set_y(bottom_right, y), empty, canvas_cols, canvas_rows, generation, } } /// Get `n`th col of `area` or its last one. #[inline] pub fn nth_col(&self, n: usize) -> Self { let Self { offset, upper_left, bottom_right, empty, canvas_cols, canvas_rows, generation, } = *self; let (max_x, _) = bottom_right; let n = std::cmp::min(n, self.width()); if self.is_empty() || max_x < (get_x(upper_left) + n) { return self.into_empty(); } let x = std::cmp::min(max_x, get_x(upper_left) + n); Self { offset: pos_inc(offset, (x, 0)), upper_left: set_x(upper_left, x), bottom_right: set_x(bottom_right, x), empty, canvas_cols, canvas_rows, generation, } } /// Place box given by `(width, height)` in corner of `area` pub fn place_inside(&self, (width, height): (usize, usize), upper: bool, left: bool) -> Self { if self.is_empty() || width < 3 || height < 3 { return *self; } let (upper_x, upper_y) = self.upper_left; let (max_x, max_y) = self.bottom_right; let x = if upper { upper_x + 2 } else { max_x.saturating_sub(2).saturating_sub(width) }; let y = if left { upper_y + 2 } else { max_y.saturating_sub(2).saturating_sub(height) }; let upper_left = (std::cmp::min(x, max_x), std::cmp::min(y, max_y)); let bottom_right = ( std::cmp::min(x + width, max_x), std::cmp::min(y + height, max_y), ); Self { offset: pos_inc( self.offset, ( (get_x(upper_left) - get_x(self.upper_left)), (get_y(upper_left) - get_y(self.upper_left)), ), ), upper_left, bottom_right, empty: self.empty, canvas_cols: self.canvas_cols, canvas_rows: self.canvas_rows, generation: self.generation, } } /// Place given area of dimensions `(width, height)` inside `area` according /// to given alignment pub fn align_inside( &self, (width, height): (usize, usize), horizontal_alignment: Alignment, vertical_alignment: Alignment, ) -> Self { if self.is_empty() || width == 0 || height == 0 { return *self; } let (top_x, width) = match horizontal_alignment { Alignment::Center => ( { std::cmp::max(self.width() / 2, width / 2) - width / 2 }, width, ), Alignment::Start => (0, self.width().min(width)), Alignment::End => (self.width().saturating_sub(width), self.width().min(width)), Alignment::Fill => (0, self.width()), }; let (top_y, height) = match vertical_alignment { Alignment::Center => ( { std::cmp::max(self.height() / 2, height / 2) - height / 2 }, self.height().min(height), ), Alignment::Start => (0, self.height().min(height)), Alignment::End => (self.height().saturating_sub(height), self.height()), Alignment::Fill => (0, self.height()), }; self.skip(top_x, top_y).take(width, height) } /// Place box given by `dimensions` in center of `area` #[inline] pub fn center_inside(&self, dimensions: (usize, usize)) -> Self { self.align_inside(dimensions, Alignment::Center, Alignment::Center) } #[inline] pub fn contains(&self, other: Self) -> bool { debug_assert_eq!(self.generation, other.generation); if self.is_empty() { return false; } else if other.is_empty() { return true; } get_y(other.bottom_right) <= get_y(self.bottom_right) && get_x(other.upper_left) >= get_x(self.upper_left) && get_y(other.upper_left) >= get_y(self.upper_left) && get_x(other.bottom_right) <= get_x(self.bottom_right) } /// Skip `n` rows and return the remaining area. /// Return value will be an empty area if `n` is more than the height. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Skip first two rows: /// let body = area.skip_rows(2); /// assert_eq!(body.height(), 18); /// ``` #[inline] pub fn skip_rows(&self, n: usize) -> Self { let n = std::cmp::min(n, self.height()); if self.is_empty() || self.upper_left.1 + n > self.bottom_right.1 { return self.into_empty(); } Self { offset: pos_inc(self.offset, (0, n)), upper_left: pos_inc(self.upper_left, (0, n)), ..*self } } /// Skip the last `n` rows and return the remaining area. /// Return value will be an empty area if `n` is more than the height. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Take only first two rows (equivalent to area.take_rows(2)) /// let header = area.skip_rows_from_end(18); /// assert_eq!(header.height(), 2); /// assert_eq!(header, area.take_rows(2)); /// ``` #[inline] pub fn skip_rows_from_end(&self, n: usize) -> Self { let n = std::cmp::min(n, self.height()); if self.is_empty() || self.bottom_right.1 < n { return self.into_empty(); } Self { bottom_right: (self.bottom_right.0, self.bottom_right.1 - n), ..*self } } /// Skip the first `n` rows and return the remaining area. /// Return value will be an empty area if `n` is more than the width. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Skip first two columns /// let indent = area.skip_cols(2); /// assert_eq!(indent.width(), 118); /// ``` #[inline] pub fn skip_cols(&self, n: usize) -> Self { let n = std::cmp::min(n, self.width()); if self.is_empty() || self.bottom_right.0 < self.upper_left.0 + n { return self.into_empty(); } Self { offset: pos_inc(self.offset, (n, 0)), upper_left: pos_inc(self.upper_left, (n, 0)), ..*self } } /// Skip the last `n` rows and return the remaining area. /// Return value will be an empty area if `n` is more than the width. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Skip last two columns /// let indent = area.skip_cols_from_end(2); /// assert_eq!(indent.width(), 118); /// assert_eq!(indent, area.take_cols(118)); /// ``` #[inline] pub fn skip_cols_from_end(&self, n: usize) -> Self { let n = std::cmp::min(n, self.width()); if self.is_empty() || self.bottom_right.0 < n { return self.into_empty(); } Self { bottom_right: (self.bottom_right.0 - n, self.bottom_right.1), ..*self } } /// Shortcut for using `Area::skip_cols` and `Area::skip_rows` together. #[inline] pub fn skip(&self, n_cols: usize, n_rows: usize) -> Self { self.skip_cols(n_cols).skip_rows(n_rows) } /// Take the first `n` rows and return the remaining area. /// Return value will be an empty area if `n` is more than the height. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Take only first two rows /// let header = area.take_rows(2); /// assert_eq!(header.height(), 2); /// ``` #[inline] pub fn take_rows(&self, n: usize) -> Self { let n = std::cmp::min(n, self.height()); if self.is_empty() || self.bottom_right.1 < (self.height() - n) { return self.into_empty(); } Self { bottom_right: ( self.bottom_right.0, self.bottom_right.1 - (self.height() - n), ), ..*self } } /// Take the first `n` columns and return the remaining area. /// Return value will be an empty area if `n` is more than the width. /// /// # Examples /// /// ```rust /// # use meli::terminal::{Screen, Virtual, Area}; /// # let mut screen = Screen::::new(); /// # assert!(screen.resize(120, 20)); /// # let area = screen.area(); /// assert_eq!(area.width(), 120); /// assert_eq!(area.height(), 20); /// // Take only first two columns /// let header = area.take_cols(2); /// assert_eq!(header.width(), 2); /// ``` #[inline] pub fn take_cols(&self, n: usize) -> Self { let n = std::cmp::min(n, self.width()); if self.is_empty() || self.bottom_right.0 < (self.width() - n) { return self.into_empty(); } Self { bottom_right: ( self.bottom_right.0 - (self.width() - n), self.bottom_right.1, ), ..*self } } /// Shortcut for using `Area::take_cols` and `Area::take_rows` together. #[inline] pub fn take(&self, n_cols: usize, n_rows: usize) -> Self { self.take_cols(n_cols).take_rows(n_rows) } #[inline] pub const fn upper_left(&self) -> Pos { self.upper_left } #[inline] pub const fn bottom_right(&self) -> Pos { self.bottom_right } #[inline] pub const fn offset(&self) -> Pos { self.offset } #[inline] pub const fn generation(&self) -> ScreenGeneration { self.generation } #[inline] pub const fn new_empty(generation: ScreenGeneration) -> Self { Self { offset: (0, 0), upper_left: (0, 0), bottom_right: (0, 0), canvas_rows: 0, canvas_cols: 0, empty: true, generation, } } #[inline] pub const fn into_empty(self) -> Self { Self { offset: (0, 0), upper_left: (0, 0), bottom_right: (0, 0), empty: true, ..self } } #[inline] pub const fn is_empty(&self) -> bool { self.empty || (self.upper_left.0 > self.bottom_right.0 || self.upper_left.1 > self.bottom_right.1) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_skip_rows() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.height() { assert_eq!(area.skip_rows(i).height(), area.height() - i); assert!(area.contains(area.skip_rows(i))); if i < area.height() { assert!(!area.take_rows(i).contains(area.skip_rows(i))); } else { assert!(area.take_rows(i).contains(area.skip_rows(i))); } } assert!(area.skip_rows(area.height()).is_empty()); assert_eq!(area.skip_rows(0), area); } #[test] fn test_skip_rows_from_end() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.height() { assert_eq!(area.skip_rows_from_end(i).height(), area.height() - i); assert!(area.contains(area.skip_rows_from_end(i))); } assert!(area.skip_rows_from_end(area.height()).is_empty()); assert_eq!(area.skip_rows_from_end(0), area); } #[test] fn test_skip_cols() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.width() { assert_eq!(area.skip_cols(i).width(), area.width() - i); assert!(area.contains(area.skip_cols(i))); if i < area.width() { assert!(!area.take_cols(i).contains(area.skip_cols(i))); } else { assert!(area.take_cols(i).contains(area.skip_cols(i))); } } assert!(area.skip_cols(area.width()).is_empty()); assert_eq!(area.skip_cols(0), area); } #[test] fn test_skip_cols_from_end() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.width() { assert_eq!(area.skip_cols_from_end(i).width(), area.width() - i); assert!(area.contains(area.skip_cols_from_end(i))); } assert!(area.skip_cols_from_end(area.width()).is_empty()); assert_eq!(area.skip_cols_from_end(0), area); } #[test] fn test_take_rows() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.height() { assert_eq!(area.take_rows(i).height(), i); assert!(area.contains(area.take_rows(i))); } assert!(area.take_rows(0).is_empty()); assert_eq!(area.take_rows(area.height()), area); } #[test] fn test_take_cols() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 1..=area.width() { assert_eq!(area.take_cols(i).width(), i); assert!(area.contains(area.take_cols(i))); } assert!(area.take_cols(0).is_empty()); assert_eq!(area.take_cols(area.width()), area); } #[test] fn test_nth_area() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for i in 0..area.width() { assert_eq!(area.nth_col(i).width(), 1); assert!(area.contains(area.nth_col(i))); if i + 1 == area.width() { assert!(area.nth_col(i).contains(area.nth_col(i + 1))); } else { assert!(!area.nth_col(i).contains(area.nth_col(i + 1))); } } for i in 0..area.height() { assert_eq!(area.nth_row(i).height(), 1); assert!(area.contains(area.nth_row(i))); if i + 1 == area.height() { assert!(area.nth_row(i).contains(area.nth_row(i + 1))); } else { assert!(!area.nth_row(i).contains(area.nth_row(i + 1))); } } } #[test] fn test_place_inside_area() { let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); for width in 0..area.width() { for height in 0..area.height() { for upper in [true, false] { for left in [true, false] { let inner = area.place_inside((width, height), upper, left); assert!(area.contains(inner)); if (3..area.height() - 2).contains(&height) && (3..area.width() - 2).contains(&width) { assert_ne!(area, inner); } } } } } } #[test] fn test_align_inside_area() { use Alignment::{Center, End, Fill, Start}; const ALIGNMENTS: [Alignment; 4] = [Fill, Start, End, Center]; let mut screen = Screen::::new(); assert!(screen.resize(120, 20)); let area = screen.area(); assert_eq!(area.width(), 120); assert_eq!(area.height(), 20); // Ask for all subsets that area has: for width in 1..=area.width() { for height in 1..=area.height() { for horz in ALIGNMENTS { for vert in ALIGNMENTS { let inner = area.align_inside((width, height), horz, vert); assert!(area.contains(inner)); assert!(!inner.is_empty()); match (horz, vert) { (Fill, Fill) => { assert_eq!(area, inner); } (Fill, _) => { assert_eq!(inner.width(), area.width()); assert_eq!(inner.height(), height); } (_, Fill) => { assert_eq!(inner.height(), area.height()); assert_eq!(inner.width(), width); } _ => { assert_eq!((width, height), inner.size()); if (width, height) != area.size() { assert_ne!(area, inner); } } } } } } } // Ask for more width/height than area has: for width in 1..=(2 * area.width()) { for height in 1..=(2 * area.height()) { for horz in ALIGNMENTS { for vert in ALIGNMENTS { let inner = area.align_inside((width, height), horz, vert); assert!(area.contains(inner)); assert!(!inner.is_empty()); match (horz, vert) { (Fill, Fill) => { assert_eq!(area, inner); } (Fill, _) => { assert_eq!(inner.width(), area.width()); assert!( height >= inner.height() && area.height() >= inner.height() ); } (_, Fill) => { assert_eq!(inner.height(), area.height()); assert!(width >= inner.width() && area.width() >= inner.width()); } _ => { assert!( height >= inner.height() && area.height() >= inner.height() ); assert!(width >= inner.width() && area.width() >= inner.width()); } } } } } } } }