1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! Drag-to-select over a scrolled body of lines.
//!
//! One mechanic for every scrolled text body that copies on drag: a left
//! press over the body anchors a selection, a drag extends it (clamped to the
//! body, so dragging past an edge selects up to that edge), and the release
//! yields the selected text. Positions are body lines, not screen rows, so the
//! highlight stays on its text while the body scrolls. Owners decide which
//! presses belong to other controls (scrollbars, copy targets) and what to do
//! with the copied text.
use ratatui::{buffer::Buffer, layout::Rect, text::Line};
use super::{
copy_interaction::{selection_position, selection_position_clamped},
text_selection::{highlight_selection, SelectionPosition, TextSelection},
};
/// A body as painted: its screen area, the body line in its top row, and
/// every body line (line 0 is the first body line, not the first visible one).
#[derive(Clone, Copy, Debug)]
pub(super) struct SelectionBody<'a> {
pub(super) area: Rect,
pub(super) top_line: usize,
pub(super) lines: &'a [Line<'a>],
}
impl SelectionBody<'_> {
fn clamped_position(self, column: u16, row: u16) -> Option<SelectionPosition> {
selection_position_clamped(self.area, self.top_line, column, row)
}
}
/// Selection state for one body. Owners forward left-button press, drag, and
/// release, and paint [`DragSelection::highlight`] over the body.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct DragSelection {
selection: Option<TextSelection>,
/// A press anchored in the body is still held, so drags extend it.
dragging: bool,
}
impl DragSelection {
/// A selection is anchored or highlighted.
pub(super) fn is_active(self) -> bool {
self.selection.is_some()
}
/// Drops the selection, e.g. when the body text changed underneath it or
/// another control took the press.
pub(super) fn clear(&mut self) {
*self = Self::default();
}
/// Anchors a selection when the press lands on body text; any other press
/// drops the previous highlight.
pub(super) fn press(&mut self, body: SelectionBody<'_>, column: u16, row: u16) {
self.selection = selection_position(body.area, body.top_line, column, row)
.filter(|_| !body.lines.is_empty())
.map(TextSelection::new);
self.dragging = self.selection.is_some();
}
pub(super) fn drag(&mut self, body: SelectionBody<'_>, column: u16, row: u16) {
if self.dragging {
self.extend(body, column, row);
}
}
/// Ends the drag and returns the text it selected. A click without
/// movement selects nothing and drops the anchor; a selection that copied
/// text stays highlighted until the next press.
pub(super) fn release(
&mut self,
body: SelectionBody<'_>,
column: u16,
row: u16,
) -> Option<String> {
if !std::mem::take(&mut self.dragging) {
return None;
}
self.extend(body, column, row);
let text = self
.selection
.and_then(|selection| selection.selected_text(body.lines, /*first_line*/ 0));
if text.is_none() {
self.selection = None;
}
text
}
/// Reverses the selected cells of `area`, whose top row shows `top_line`.
pub(super) fn highlight(self, buffer: &mut Buffer, area: Rect, top_line: usize) {
if let Some(selection) = self.selection {
highlight_selection(buffer, area, top_line, selection);
}
}
fn extend(&mut self, body: SelectionBody<'_>, column: u16, row: u16) {
if let (Some(selection), Some(position)) =
(self.selection.as_mut(), body.clamped_position(column, row))
{
selection.update(position);
}
}
}
#[cfg(test)]
#[path = "drag_selection_tests.rs"]
mod tests;