diff options
author | Joe Wilm <joe@jwilm.com> | 2016-12-22 13:43:06 -0500 |
---|---|---|
committer | Joe Wilm <joe@jwilm.com> | 2016-12-22 13:44:13 -0500 |
commit | 6e708d2119ce0c839a89858a42a6b124a5cf48f4 (patch) | |
tree | a4ea2078153d136536587e04922f4ec841860298 /src/selection.rs | |
parent | fd11660c0a714852a3f477a6730d49b9694e1345 (diff) | |
download | alacritty-6e708d2119ce0c839a89858a42a6b124a5cf48f4.tar.gz alacritty-6e708d2119ce0c839a89858a42a6b124a5cf48f4.zip |
Implement visual component of mouse selections
This adds the ability to click and drag with the mouse and have the
effect of visually selecting text. The ability to copy the selection
into a clipboard buffer is not yet implemented.
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 + }); + } +} |