mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-18 13:55:23 -05:00
380 lines
12 KiB
Rust
380 lines
12 KiB
Rust
// 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 index::{Point, Column, RangeInclusive, Side, Linear, Line};
|
|
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: Point,
|
|
end: Point,
|
|
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: Point, 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 {
|
|
if start_side == end_side {
|
|
return None;
|
|
} else {
|
|
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: Point,
|
|
tail: Point,
|
|
|
|
/// The type says whether ends are included or not.
|
|
ty: SpanType,
|
|
}
|
|
|
|
impl Span {
|
|
pub fn to_locations(&self, cols: Column) -> (Point, Point) {
|
|
match self.ty {
|
|
SpanType::Inclusive => (self.front, self.tail),
|
|
SpanType::Exclusive => {
|
|
(Span::wrap_start(self.front, cols), Span::wrap_end(self.tail, cols))
|
|
},
|
|
SpanType::ExcludeFront => (Span::wrap_start(self.front, cols), self.tail),
|
|
SpanType::ExcludeTail => (self.front, Span::wrap_end(self.tail, cols))
|
|
}
|
|
}
|
|
|
|
fn wrap_start(mut start: Point, cols: Column) -> Point {
|
|
if start.col == cols - 1 {
|
|
Point {
|
|
line: start.line + 1,
|
|
col: Column(0),
|
|
}
|
|
} else {
|
|
start.col += 1;
|
|
start
|
|
}
|
|
}
|
|
|
|
fn wrap_end(end: Point, cols: Column) -> Point {
|
|
if end.col == Column(0) && end.line != Line(0) {
|
|
Point {
|
|
line: end.line - 1,
|
|
col: cols
|
|
}
|
|
} else {
|
|
Point {
|
|
line: end.line,
|
|
col: end.col - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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))
|
|
};
|
|
|
|
RangeInclusive::new(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, Point};
|
|
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 = Point { 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 = Point { 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(Point::new(Line(0), Column(0)), Side::Right);
|
|
selection.update(Point::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(Point::new(Line(0), Column(1)), Side::Left);
|
|
selection.update(Point::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(Point::new(Line(1), Column(1)), Side::Right);
|
|
selection.update(Point::new(Line(0), Column(1)), Side::Right);
|
|
|
|
assert_eq!(selection.span().unwrap(), Span {
|
|
front: Point::new(Line(0), Column(1)),
|
|
tail: Point::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(Point::new(Line(0), Column(1)), Side::Right);
|
|
selection.update(Point::new(Line(1), Column(1)), Side::Right);
|
|
selection.update(Point::new(Line(1), Column(0)), Side::Right);
|
|
|
|
assert_eq!(selection.span().unwrap(), Span {
|
|
front: Point::new(Line(0), Column(1)),
|
|
tail: Point::new(Line(1), Column(0)),
|
|
ty: SpanType::ExcludeFront
|
|
});
|
|
}
|
|
}
|