diff options
Diffstat (limited to 'src/selection.rs')
-rw-r--r-- | src/selection.rs | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/src/selection.rs b/src/selection.rs new file mode 100644 index 00000000..6c927967 --- /dev/null +++ b/src/selection.rs @@ -0,0 +1,340 @@ +// Copyright 2016 Joe Wilm, The Alacritty Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! 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::mem; +use std::ops::RangeInclusive; + +use index::{Location, Column, Side, Linear}; +use grid::ToRange; + +/// The area selected +/// +/// Contains all the logic for processing mouse position events and providing +/// necessary info the the renderer. +#[derive(Debug)] +pub enum Selection { + /// No current selection or start of a selection + Empty, + + Active { + start: Location, + end: Location, + start_side: Side, + end_side: Side + }, +} + +impl Default for Selection { + fn default() -> Selection { + Selection::Empty + } +} + +impl Selection { + /// Create a selection in the default state + #[inline] + pub fn new() -> Selection { + Default::default() + } + + /// Clear the active selection + pub fn clear(&mut self) { + mem::replace(self, Selection::Empty); + } + + pub fn is_empty(&self) -> bool { + match *self { + Selection::Empty => true, + _ => false + } + } + + pub fn update(&mut self, location: Location, side: Side) { + let selection = mem::replace(self, Selection::Empty); + let selection = match selection { + Selection::Empty => { + // Start a selection + Selection::Active { + start: location, + end: location, + start_side: side, + end_side: side + } + }, + Selection::Active { start, start_side, .. } => { + // Update ends + Selection::Active { + start: start, + start_side: start_side, + end: location, + end_side: side + } + } + }; + + mem::replace(self, selection); + } + + pub fn span(&self) -> Option<Span> { + match *self { + Selection::Active {ref start, ref end, ref start_side, ref end_side } => { + let (front, tail, front_side, tail_side) = if *start > *end { + // Selected upward; start/end are swapped + (end, start, end_side, start_side) + } else { + // Selected downward; no swapping + (start, end, start_side, end_side) + }; + + debug_assert!(!(tail < front)); + + // Single-cell selections are a special case + if start == end && start_side != end_side { + return Some(Span { + ty: SpanType::Inclusive, + front: *front, + tail: *tail + }); + } + + // The other special case is two adjacent cells with no + // selection: [ B][E ] or [ E][B ] + let adjacent = tail.line == front.line && tail.col - front.col == Column(1); + if adjacent && *front_side == Side::Right && *tail_side == Side::Left { + return None; + } + + Some(match (*front_side, *tail_side) { + // [FX][XX][XT] + (Side::Left, Side::Right) => Span { + front: *front, + tail: *tail, + ty: SpanType::Inclusive + }, + // [ F][XX][T ] + (Side::Right, Side::Left) => Span { + front: *front, + tail: *tail, + ty: SpanType::Exclusive + }, + // [FX][XX][T ] + (Side::Left, Side::Left) => Span { + front: *front, + tail: *tail, + ty: SpanType::ExcludeTail + }, + // [ F][XX][XT] + (Side::Right, Side::Right) => Span { + front: *front, + tail: *tail, + ty: SpanType::ExcludeFront + }, + }) + }, + Selection::Empty => None + } + } +} + +/// How to interpret the locations of a Span. +#[derive(Debug, Eq, PartialEq)] +pub enum SpanType { + /// Includes the beginning and end locations + Inclusive, + + /// Exclude both beginning and end + Exclusive, + + /// Excludes last cell of selection + ExcludeTail, + + /// Excludes first cell of selection + ExcludeFront, +} + +/// Represents a span of selected cells +#[derive(Debug, Eq, PartialEq)] +pub struct Span { + front: Location, + tail: Location, + + /// The type says whether ends are included or not. + ty: SpanType, +} + +impl Span { + #[inline] + fn exclude_start(start: Linear) -> Linear { + start + 1 + } + + #[inline] + fn exclude_end(end: Linear) -> Linear { + if end > Linear(0) { + end - 1 + } else { + end + } + } +} + +impl ToRange for Span { + fn to_range(&self, cols: Column) -> RangeInclusive<Linear> { + let start = Linear(self.front.line.0 * cols.0 + self.front.col.0); + let end = Linear(self.tail.line.0 * cols.0 + self.tail.col.0); + + let (start, end) = match self.ty { + SpanType::Inclusive => (start, end), + SpanType::Exclusive => (Span::exclude_start(start), Span::exclude_end(end)), + SpanType::ExcludeFront => (Span::exclude_start(start), end), + SpanType::ExcludeTail => (start, Span::exclude_end(end)) + }; + + start...end + } +} + +/// 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 comletely 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 test { + use index::{Line, Column, Side, Location}; + use super::{Selection, Span, SpanType}; + + /// Test case of single cell selection + /// + /// 1. [ ] + /// 2. [B ] + /// 3. [BE] + #[test] + fn single_cell_left_to_right() { + let location = Location { line: Line(0), col: Column(0) }; + let mut selection = Selection::Empty; + selection.update(location, Side::Left); + selection.update(location, Side::Right); + + assert_eq!(selection.span().unwrap(), Span { + ty: SpanType::Inclusive, + front: location, + tail: location + }); + } + + /// Test case of single cell selection + /// + /// 1. [ ] + /// 2. [ B] + /// 3. [EB] + #[test] + fn single_cell_right_to_left() { + let location = Location { line: Line(0), col: Column(0) }; + let mut selection = Selection::Empty; + selection.update(location, Side::Right); + selection.update(location, Side::Left); + + assert_eq!(selection.span().unwrap(), Span { + ty: SpanType::Inclusive, + front: location, + tail: location + }); + } + + /// 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::Empty; + selection.update(Location::new(Line(0), Column(0)), Side::Right); + selection.update(Location::new(Line(0), Column(1)), Side::Left); + + assert_eq!(selection.span(), 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::Empty; + selection.update(Location::new(Line(0), Column(1)), Side::Left); + selection.update(Location::new(Line(0), Column(0)), Side::Right); + + assert_eq!(selection.span(), None); + } + + /// Test selection across adjacent lines + /// + /// + /// 1. [ ][ ][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 2. [ ][ ][ ][ ][ ] + /// [ ][ B][ ][ ][ ] + /// 3. [ ][ E][XX][XX][XX] + /// [XX][XB][ ][ ][ ] + #[test] + fn across_adjacent_lines_upward_final_cell_exclusive() { + let mut selection = Selection::Empty; + selection.update(Location::new(Line(1), Column(1)), Side::Right); + selection.update(Location::new(Line(0), Column(1)), Side::Right); + + assert_eq!(selection.span().unwrap(), Span { + front: Location::new(Line(0), Column(1)), + tail: Location::new(Line(1), Column(1)), + ty: SpanType::ExcludeFront + }); + } + + /// Test selection across adjacent lines + /// + /// + /// 1. [ ][ ][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 2. [ ][ B][ ][ ][ ] + /// [ ][ ][ ][ ][ ] + /// 3. [ ][ B][XX][XX][XX] + /// [XX][XE][ ][ ][ ] + /// 4. [ ][ B][XX][XX][XX] + /// [XE][ ][ ][ ][ ] + #[test] + fn selection_bigger_then_smaller() { + let mut selection = Selection::Empty; + selection.update(Location::new(Line(0), Column(1)), Side::Right); + selection.update(Location::new(Line(1), Column(1)), Side::Right); + selection.update(Location::new(Line(1), Column(0)), Side::Right); + + assert_eq!(selection.span().unwrap(), Span { + front: Location::new(Line(0), Column(1)), + tail: Location::new(Line(1), Column(0)), + ty: SpanType::ExcludeFront + }); + } +} |