| //! State management for a selection in the grid. |
| //! |
| //! A selection should start when the mouse is clicked, and it should be |
| //! finalized when the button is released. The selection should be cleared |
| //! when text is added/removed/scrolled on the screen. The selection should |
| //! also be cleared if the user clicks off of the selection. |
| |
| use std::convert::TryFrom; |
| use std::mem; |
| use std::ops::{Bound, Range, RangeBounds}; |
| |
| use crate::grid::Dimensions; |
| use crate::index::{Column, Line, Point, Side}; |
| use crate::term::Term; |
| |
| /// A Point and side within that point. |
| #[derive(Debug, Copy, Clone, PartialEq)] |
| struct Anchor { |
| point: Point<usize>, |
| side: Side, |
| } |
| |
| impl Anchor { |
| fn new(point: Point<usize>, side: Side) -> Anchor { |
| Anchor { point, side } |
| } |
| } |
| |
| /// Represents a range of selected cells. |
| #[derive(Copy, Clone, Debug, Eq, PartialEq)] |
| pub struct SelectionRange<L = usize> { |
| /// Start point, top left of the selection. |
| pub start: Point<L>, |
| /// End point, bottom right of the selection. |
| pub end: Point<L>, |
| /// Whether this selection is a block selection. |
| pub is_block: bool, |
| } |
| |
| impl<L> SelectionRange<L> { |
| pub fn new(start: Point<L>, end: Point<L>, is_block: bool) -> Self { |
| Self { start, end, is_block } |
| } |
| |
| pub fn contains(&self, col: Column, line: L) -> bool |
| where |
| L: PartialEq + PartialOrd, |
| { |
| self.start.line <= line |
| && self.end.line >= line |
| && (self.start.col <= col || (self.start.line != line && !self.is_block)) |
| && (self.end.col >= col || (self.end.line != line && !self.is_block)) |
| } |
| } |
| |
| /// Different kinds of selection. |
| #[derive(Debug, Copy, Clone, PartialEq)] |
| pub enum SelectionType { |
| Simple, |
| Block, |
| Semantic, |
| Lines, |
| } |
| |
| /// Describes a region of a 2-dimensional area. |
| /// |
| /// Used to track a text selection. There are four supported modes, each with its own constructor: |
| /// [`simple`], [`block`], [`semantic`], and [`lines`]. The [`simple`] mode precisely tracks which |
| /// cells are selected without any expansion. [`block`] will select rectangular regions. |
| /// [`semantic`] mode expands the initial selection to the nearest semantic escape char in either |
| /// direction. [`lines`] will always select entire lines. |
| /// |
| /// Calls to [`update`] operate different based on the selection kind. The [`simple`] and [`block`] |
| /// mode do nothing special, simply track points and sides. [`semantic`] will continue to expand |
| /// out to semantic boundaries as the selection point changes. Similarly, [`lines`] will always |
| /// expand the new point to encompass entire lines. |
| /// |
| /// [`simple`]: enum.Selection.html#method.simple |
| /// [`block`]: enum.Selection.html#method.block |
| /// [`semantic`]: enum.Selection.html#method.semantic |
| /// [`lines`]: enum.Selection.html#method.lines |
| /// [`update`]: enum.Selection.html#method.update |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct Selection { |
| pub ty: SelectionType, |
| region: Range<Anchor>, |
| } |
| |
| impl Selection { |
| pub fn new(ty: SelectionType, location: Point<usize>, side: Side) -> Selection { |
| Self { |
| region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) }, |
| ty, |
| } |
| } |
| |
| /// Update the end of the selection. |
| pub fn update(&mut self, point: Point<usize>, side: Side) { |
| self.region.end = Anchor::new(point, side); |
| } |
| |
| pub fn rotate<D: Dimensions>( |
| mut self, |
| dimensions: &D, |
| range: &Range<Line>, |
| delta: isize, |
| ) -> Option<Selection> { |
| let num_lines = dimensions.screen_lines().0; |
| let num_cols = dimensions.cols().0; |
| let range_bottom = range.start.0; |
| let range_top = range.end.0; |
| |
| let (mut start, mut end) = (&mut self.region.start, &mut self.region.end); |
| if Selection::points_need_swap(start.point, end.point) { |
| mem::swap(&mut start, &mut end); |
| } |
| |
| // Rotate start of selection. |
| if (start.point.line < range_top || range_top == num_lines) |
| && start.point.line >= range_bottom |
| { |
| start.point.line = usize::try_from(start.point.line as isize + delta).unwrap_or(0); |
| |
| // If end is within the same region, delete selection once start rotates out. |
| if start.point.line < range_bottom && end.point.line >= range_bottom { |
| return None; |
| } |
| |
| // Clamp selection to start of region. |
| if start.point.line >= range_top && range_top != num_lines { |
| if self.ty != SelectionType::Block { |
| start.point.col = Column(0); |
| start.side = Side::Left; |
| } |
| start.point.line = range_top - 1; |
| } |
| } |
| |
| // Rotate end of selection. |
| if (end.point.line < range_top || range_top == num_lines) && end.point.line >= range_bottom |
| { |
| end.point.line = usize::try_from(end.point.line as isize + delta).unwrap_or(0); |
| |
| // Delete selection if end has overtaken the start. |
| if end.point.line > start.point.line { |
| return None; |
| } |
| |
| // Clamp selection to end of region. |
| if end.point.line < range_bottom { |
| if self.ty != SelectionType::Block { |
| end.point.col = Column(num_cols - 1); |
| end.side = Side::Right; |
| } |
| end.point.line = range_bottom; |
| } |
| } |
| |
| Some(self) |
| } |
| |
| pub fn is_empty(&self) -> bool { |
| match self.ty { |
| SelectionType::Simple => { |
| let (mut start, mut end) = (self.region.start, self.region.end); |
| if Self::points_need_swap(start.point, end.point) { |
| mem::swap(&mut start, &mut end); |
| } |
| |
| // Simple selection is empty when the points are identical |
| // or two adjacent cells have the sides right -> left. |
| start == end |
| || (start.side == Side::Right |
| && end.side == Side::Left |
| && (start.point.line == end.point.line) |
| && start.point.col + 1 == end.point.col) |
| }, |
| SelectionType::Block => { |
| let (start, end) = (self.region.start, self.region.end); |
| |
| // Block selection is empty when the points' columns and sides are identical |
| // or two cells with adjacent columns have the sides right -> left, |
| // regardless of their lines |
| (start.point.col == end.point.col && start.side == end.side) |
| || (start.point.col + 1 == end.point.col |
| && start.side == Side::Right |
| && end.side == Side::Left) |
| || (end.point.col + 1 == start.point.col |
| && start.side == Side::Left |
| && end.side == Side::Right) |
| }, |
| SelectionType::Semantic | SelectionType::Lines => false, |
| } |
| } |
| |
| /// Check whether selection contains any point in a given range. |
| pub fn intersects_range<R: RangeBounds<usize>>(&self, range: R) -> bool { |
| let mut start = self.region.start.point.line; |
| let mut end = self.region.end.point.line; |
| |
| if Self::points_need_swap(self.region.start.point, self.region.end.point) { |
| mem::swap(&mut start, &mut end); |
| } |
| |
| let range_start = match range.start_bound() { |
| Bound::Included(&range_start) => range_start, |
| Bound::Excluded(&range_start) => range_start.saturating_add(1), |
| Bound::Unbounded => 0, |
| }; |
| |
| let range_end = match range.end_bound() { |
| Bound::Included(&range_end) => range_end, |
| Bound::Excluded(&range_end) => range_end.saturating_sub(1), |
| Bound::Unbounded => usize::max_value(), |
| }; |
| |
| range_start <= start && range_end >= end |
| } |
| |
| /// Expand selection sides to include all cells. |
| pub fn include_all(&mut self) { |
| let (start, end) = (self.region.start.point, self.region.end.point); |
| let (start_side, end_side) = match self.ty { |
| SelectionType::Block |
| if start.col > end.col || (start.col == end.col && start.line < end.line) => |
| { |
| (Side::Right, Side::Left) |
| }, |
| SelectionType::Block => (Side::Left, Side::Right), |
| _ if Self::points_need_swap(start, end) => (Side::Right, Side::Left), |
| _ => (Side::Left, Side::Right), |
| }; |
| |
| self.region.start.side = start_side; |
| self.region.end.side = end_side; |
| } |
| |
| /// Convert selection to grid coordinates. |
| pub fn to_range<T>(&self, term: &Term<T>) -> Option<SelectionRange> { |
| let grid = term.grid(); |
| let num_cols = grid.cols(); |
| |
| // Order start above the end. |
| let mut start = self.region.start; |
| let mut end = self.region.end; |
| |
| if Self::points_need_swap(start.point, end.point) { |
| mem::swap(&mut start, &mut end); |
| } |
| |
| // Clamp to inside the grid buffer. |
| let is_block = self.ty == SelectionType::Block; |
| let (start, end) = Self::grid_clamp(start, end, is_block, grid.total_lines()).ok()?; |
| |
| match self.ty { |
| SelectionType::Simple => self.range_simple(start, end, num_cols), |
| SelectionType::Block => self.range_block(start, end), |
| SelectionType::Semantic => Self::range_semantic(term, start.point, end.point), |
| SelectionType::Lines => Self::range_lines(term, start.point, end.point), |
| } |
| } |
| |
| /// Bring start and end points in the correct order. |
| fn points_need_swap(start: Point<usize>, end: Point<usize>) -> bool { |
| start.line < end.line || start.line == end.line && start.col > end.col |
| } |
| |
| /// Clamp selection inside grid to prevent OOB. |
| fn grid_clamp( |
| mut start: Anchor, |
| end: Anchor, |
| is_block: bool, |
| lines: usize, |
| ) -> Result<(Anchor, Anchor), ()> { |
| // Clamp selection inside of grid to prevent OOB. |
| if start.point.line >= lines { |
| // Remove selection if it is fully out of the grid. |
| if end.point.line >= lines { |
| return Err(()); |
| } |
| |
| // Clamp to grid if it is still partially visible. |
| if !is_block { |
| start.side = Side::Left; |
| start.point.col = Column(0); |
| } |
| start.point.line = lines - 1; |
| } |
| |
| Ok((start, end)) |
| } |
| |
| fn range_semantic<T>( |
| term: &Term<T>, |
| mut start: Point<usize>, |
| mut end: Point<usize>, |
| ) -> Option<SelectionRange> { |
| if start == end { |
| if let Some(matching) = term.bracket_search(start) { |
| if (matching.line == start.line && matching.col < start.col) |
| || (matching.line > start.line) |
| { |
| start = matching; |
| } else { |
| end = matching; |
| } |
| |
| return Some(SelectionRange { start, end, is_block: false }); |
| } |
| } |
| |
| start = term.semantic_search_left(start); |
| end = term.semantic_search_right(end); |
| |
| Some(SelectionRange { start, end, is_block: false }) |
| } |
| |
| fn range_lines<T>( |
| term: &Term<T>, |
| mut start: Point<usize>, |
| mut end: Point<usize>, |
| ) -> Option<SelectionRange> { |
| start = term.line_search_left(start); |
| end = term.line_search_right(end); |
| |
| Some(SelectionRange { start, end, is_block: false }) |
| } |
| |
| fn range_simple( |
| &self, |
| mut start: Anchor, |
| mut end: Anchor, |
| num_cols: Column, |
| ) -> Option<SelectionRange> { |
| if self.is_empty() { |
| return None; |
| } |
| |
| // Remove last cell if selection ends to the left of a cell. |
| if end.side == Side::Left && start.point != end.point { |
| // Special case when selection ends to left of first cell. |
| if end.point.col == Column(0) { |
| end.point.col = num_cols - 1; |
| end.point.line += 1; |
| } else { |
| end.point.col -= 1; |
| } |
| } |
| |
| // Remove first cell if selection starts at the right of a cell. |
| if start.side == Side::Right && start.point != end.point { |
| start.point.col += 1; |
| |
| // Wrap to next line when selection starts to the right of last column. |
| if start.point.col == num_cols { |
| start.point = Point::new(start.point.line.saturating_sub(1), Column(0)); |
| } |
| } |
| |
| Some(SelectionRange { start: start.point, end: end.point, is_block: false }) |
| } |
| |
| fn range_block(&self, mut start: Anchor, mut end: Anchor) -> Option<SelectionRange> { |
| if self.is_empty() { |
| return None; |
| } |
| |
| // Always go top-left -> bottom-right. |
| if start.point.col > end.point.col { |
| mem::swap(&mut start.side, &mut end.side); |
| mem::swap(&mut start.point.col, &mut end.point.col); |
| } |
| |
| // Remove last cell if selection ends to the left of a cell. |
| if end.side == Side::Left && start.point != end.point && end.point.col.0 > 0 { |
| end.point.col -= 1; |
| } |
| |
| // Remove first cell if selection starts at the right of a cell. |
| if start.side == Side::Right && start.point != end.point { |
| start.point.col += 1; |
| } |
| |
| Some(SelectionRange { start: start.point, end: end.point, is_block: true }) |
| } |
| } |
| |
| /// Tests for selection. |
| /// |
| /// There are comments on all of the tests describing the selection. Pictograms |
| /// are used to avoid ambiguity. Grid cells are represented by a [ ]. Only |
| /// cells that are completely covered are counted in a selection. Ends are |
| /// represented by `B` and `E` for begin and end, respectively. A selected cell |
| /// looks like [XX], [BX] (at the start), [XB] (at the end), [XE] (at the end), |
| /// and [EX] (at the start), or [BE] for a single cell. Partially selected cells |
| /// look like [ B] and [E ]. |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use crate::config::MockConfig; |
| use crate::event::{Event, EventListener}; |
| use crate::index::{Column, Line, Point, Side}; |
| use crate::term::{SizeInfo, Term}; |
| |
| struct Mock; |
| impl EventListener for Mock { |
| fn send_event(&self, _event: Event) {} |
| } |
| |
| fn term(height: usize, width: usize) -> Term<Mock> { |
| let size = SizeInfo { |
| width: width as f32, |
| height: height as f32, |
| cell_width: 1.0, |
| cell_height: 1.0, |
| padding_x: 0.0, |
| padding_y: 0.0, |
| dpr: 1.0, |
| }; |
| Term::new(&MockConfig::default(), &size, Mock) |
| } |
| |
| /// Test case of single cell selection. |
| /// |
| /// 1. [ ] |
| /// 2. [B ] |
| /// 3. [BE] |
| #[test] |
| fn single_cell_left_to_right() { |
| let location = Point { line: 0, col: Column(0) }; |
| let mut selection = Selection::new(SelectionType::Simple, location, Side::Left); |
| selection.update(location, Side::Right); |
| |
| assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange { |
| start: location, |
| end: location, |
| is_block: false |
| }); |
| } |
| |
| /// Test case of single cell selection. |
| /// |
| /// 1. [ ] |
| /// 2. [ B] |
| /// 3. [EB] |
| #[test] |
| fn single_cell_right_to_left() { |
| let location = Point { line: 0, col: Column(0) }; |
| let mut selection = Selection::new(SelectionType::Simple, location, Side::Right); |
| selection.update(location, Side::Left); |
| |
| assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange { |
| start: location, |
| end: location, |
| is_block: false |
| }); |
| } |
| |
| /// Test adjacent cell selection from left to right. |
| /// |
| /// 1. [ ][ ] |
| /// 2. [ B][ ] |
| /// 3. [ B][E ] |
| #[test] |
| fn between_adjacent_cells_left_to_right() { |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right); |
| selection.update(Point::new(0, Column(1)), Side::Left); |
| |
| assert_eq!(selection.to_range(&term(1, 2)), None); |
| } |
| |
| /// Test adjacent cell selection from right to left. |
| /// |
| /// 1. [ ][ ] |
| /// 2. [ ][B ] |
| /// 3. [ E][B ] |
| #[test] |
| fn between_adjacent_cells_right_to_left() { |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Left); |
| selection.update(Point::new(0, Column(0)), Side::Right); |
| |
| assert_eq!(selection.to_range(&term(1, 2)), None); |
| } |
| |
| /// Test selection across adjacent lines. |
| /// |
| /// 1. [ ][ ][ ][ ][ ] |
| /// [ ][ ][ ][ ][ ] |
| /// 2. [ ][ B][ ][ ][ ] |
| /// [ ][ ][ ][ ][ ] |
| /// 3. [ ][ B][XX][XX][XX] |
| /// [XX][XE][ ][ ][ ] |
| #[test] |
| fn across_adjacent_lines_upward_final_cell_exclusive() { |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(1, Column(1)), Side::Right); |
| selection.update(Point::new(0, Column(1)), Side::Right); |
| |
| assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange { |
| start: Point::new(1, Column(2)), |
| end: Point::new(0, Column(1)), |
| is_block: false, |
| }); |
| } |
| |
| /// Test selection across adjacent lines. |
| /// |
| /// 1. [ ][ ][ ][ ][ ] |
| /// [ ][ ][ ][ ][ ] |
| /// 2. [ ][ ][ ][ ][ ] |
| /// [ ][ B][ ][ ][ ] |
| /// 3. [ ][ E][XX][XX][XX] |
| /// [XX][XB][ ][ ][ ] |
| /// 4. [ E][XX][XX][XX][XX] |
| /// [XX][XB][ ][ ][ ] |
| #[test] |
| fn selection_bigger_then_smaller() { |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Right); |
| selection.update(Point::new(1, Column(1)), Side::Right); |
| selection.update(Point::new(1, Column(0)), Side::Right); |
| |
| assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange { |
| start: Point::new(1, Column(1)), |
| end: Point::new(0, Column(1)), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn line_selection() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Lines, Point::new(0, Column(1)), Side::Left); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(9, Column(0)), |
| end: Point::new(7, Column(4)), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn semantic_selection() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Semantic, Point::new(0, Column(3)), Side::Left); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(9, Column(0)), |
| end: Point::new(7, Column(3)), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn simple_selection() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(0, Column(3)), Side::Right); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(9, Column(0)), |
| end: Point::new(7, Column(3)), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn block_selection() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Block, Point::new(0, Column(3)), Side::Right); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(9, Column(2)), |
| end: Point::new(7, Column(3)), |
| is_block: true |
| }); |
| } |
| |
| #[test] |
| fn simple_is_empty() { |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(0, Column(1)), Side::Left); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(1, Column(0)), Side::Right); |
| assert!(!selection.is_empty()); |
| } |
| |
| #[test] |
| fn block_is_empty() { |
| let mut selection = |
| Selection::new(SelectionType::Block, Point::new(0, Column(0)), Side::Right); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(0, Column(1)), Side::Left); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(0, Column(1)), Side::Right); |
| assert!(!selection.is_empty()); |
| selection.update(Point::new(1, Column(0)), Side::Right); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(1, Column(1)), Side::Left); |
| assert!(selection.is_empty()); |
| selection.update(Point::new(1, Column(1)), Side::Right); |
| assert!(!selection.is_empty()); |
| } |
| |
| #[test] |
| fn rotate_in_region_up() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(2, Column(3)), Side::Right); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(8, Column(0)), |
| end: Point::new(6, Column(3)), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn rotate_in_region_down() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Simple, Point::new(5, Column(3)), Side::Right); |
| selection.update(Point::new(8, Column(1)), Side::Left); |
| selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), -5).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(3, Column(1)), |
| end: Point::new(1, size.1 - 1), |
| is_block: false, |
| }); |
| } |
| |
| #[test] |
| fn rotate_in_region_up_block() { |
| let size = (Line(10), Column(5)); |
| let mut selection = |
| Selection::new(SelectionType::Block, Point::new(2, Column(3)), Side::Right); |
| selection.update(Point::new(5, Column(1)), Side::Right); |
| selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap(); |
| |
| assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange { |
| start: Point::new(8, Column(2)), |
| end: Point::new(6, Column(3)), |
| is_block: true, |
| }); |
| } |
| |
| #[test] |
| fn range_intersection() { |
| let mut selection = |
| Selection::new(SelectionType::Lines, Point::new(6, Column(1)), Side::Left); |
| selection.update(Point::new(3, Column(1)), Side::Right); |
| |
| assert!(selection.intersects_range(..)); |
| assert!(selection.intersects_range(2..)); |
| assert!(selection.intersects_range(2..=4)); |
| assert!(selection.intersects_range(2..=7)); |
| assert!(selection.intersects_range(4..=5)); |
| assert!(selection.intersects_range(5..8)); |
| |
| assert!(!selection.intersects_range(..=2)); |
| assert!(!selection.intersects_range(7..=8)); |
| } |
| } |