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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! Touch selection geometry shared by the input engine and window text
//! selection.
//!
//! A touch selection is one made by a long press. It differs from a pointer
//! selection in what the user needs afterwards: a grab handle at each end to
//! adjust it, since a finger cannot hover an I-beam, and an edit menu next to
//! it, since there is no right click. Base owns the gesture, the handle drag
//! and the menu lifecycle; this module carries what a presentation layer needs
//! to draw them. The handles and the menu themselves are drawn by the styled
//! layer, which also decides how large a handle's touch target is.
use gpui::{Bounds, Hsla, Pixels, Point, Window, fill, point, px, size};
/// One end of a selection, as a touch handle grabs it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SelectionEdge {
/// The end before the first selected character.
Start,
/// The end after the last selected character.
End,
}
impl SelectionEdge {
/// The end that stays put while this one is dragged.
pub const fn opposite(self) -> Self {
match self {
Self::Start => Self::End,
Self::End => Self::Start,
}
}
}
/// Where a touch selection's ends are laid out this frame, and what the
/// gesture is doing to them.
///
/// Each end is the caret line box at that end in window coordinates: zero
/// width, one line tall, at the character boundary. An empty selection has
/// both ends at the caret. Built only by Base; a presentation layer reads it
/// to draw the handles and place the edit menu.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TouchSelectionSnapshot {
start: Bounds<Pixels>,
end: Bounds<Pixels>,
/// Whether each end is inside its owner's viewport. An end scrolled out
/// of view keeps its geometry, for the drag that holds the other end,
/// but gets no handle.
start_visible: bool,
end_visible: bool,
menu_open: bool,
dragging: Option<SelectionEdge>,
}
impl TouchSelectionSnapshot {
pub(crate) const fn new(start: Bounds<Pixels>, end: Bounds<Pixels>) -> Self {
Self {
start,
end,
start_visible: true,
end_visible: true,
menu_open: false,
dragging: None,
}
}
pub(crate) const fn with_edge_visible(mut self, edge: SelectionEdge, visible: bool) -> Self {
match edge {
SelectionEdge::Start => self.start_visible = visible,
SelectionEdge::End => self.end_visible = visible,
}
self
}
pub(crate) const fn with_menu_open(mut self, menu_open: bool) -> Self {
self.menu_open = menu_open;
self
}
pub(crate) const fn with_dragging(mut self, dragging: Option<SelectionEdge>) -> Self {
self.dragging = dragging;
self
}
/// The caret line box before the first selected character.
pub const fn start(&self) -> Bounds<Pixels> {
self.start
}
/// The caret line box after the last selected character.
pub const fn end(&self) -> Bounds<Pixels> {
self.end
}
/// The caret line box at the given end.
pub const fn edge(&self, edge: SelectionEdge) -> Bounds<Pixels> {
match edge {
SelectionEdge::Start => self.start,
SelectionEdge::End => self.end,
}
}
/// Whether the given end is in view. A handle is drawn only for an end
/// that is; the menu anchors to the ends that are.
pub const fn is_edge_visible(&self, edge: SelectionEdge) -> bool {
match edge {
SelectionEdge::Start => self.start_visible,
SelectionEdge::End => self.end_visible,
}
}
/// Whether the selection is a bare caret, which gets a menu but no handles.
pub fn is_empty(&self) -> bool {
self.start == self.end
}
/// The smallest box holding the ends in view, for anchoring the edit
/// menu. `None` when the whole selection is scrolled away.
pub fn bounds(&self) -> Option<Bounds<Pixels>> {
match (self.start_visible, self.end_visible) {
(true, true) => Some(self.start.union(&self.end)),
(true, false) => Some(self.start),
(false, true) => Some(self.end),
(false, false) => None,
}
}
/// Whether the edit menu is open. It closes while a handle is dragged and
/// reopens when the drag ends.
pub const fn is_menu_open(&self) -> bool {
self.menu_open
}
/// The end currently being dragged by its handle.
pub const fn dragging(&self) -> Option<SelectionEdge> {
self.dragging
}
}
/// The grab handle at one end of a touch selection: a bar down the caret
/// line with a knob at its outer end, the start's above and the end's below.
///
/// Base paints the handle where it belongs in the paint order — inside the
/// text that owns the selection, so that whatever covers the text covers
/// the handle — and only with the selection's own color. Its shape is the
/// one every platform draws; a styled layer supplies the color.
pub struct TouchHandle;
impl TouchHandle {
/// How wide the finger may miss the knob and still take it.
pub const HIT_SIZE: Pixels = px(44.);
/// The knob's diameter.
pub const KNOB_SIZE: Pixels = px(10.);
/// The bar down the caret line.
pub const BAR_WIDTH: Pixels = px(2.);
/// Room the knob takes beyond the line, which an edit menu keeps clear.
pub const EXTENT: Pixels = px(12.);
/// The touch target, centered on the caret and reaching out past the knob.
pub fn hit_bounds(edge: SelectionEdge, caret: Bounds<Pixels>) -> Bounds<Pixels> {
let top = match edge {
SelectionEdge::Start => caret.top() - Self::EXTENT,
SelectionEdge::End => caret.top(),
};
Bounds::new(
point(caret.left() - Self::HIT_SIZE / 2., top),
size(Self::HIT_SIZE, caret.size.height + Self::EXTENT),
)
}
/// Paints the handle for `edge` on the caret line box `caret`.
pub fn paint(edge: SelectionEdge, caret: Bounds<Pixels>, color: Hsla, window: &mut Window) {
let bar = Bounds::new(
point(caret.left() - Self::BAR_WIDTH / 2., caret.top()),
size(Self::BAR_WIDTH, caret.size.height),
);
let knob_top = match edge {
SelectionEdge::Start => caret.top() - Self::KNOB_SIZE,
SelectionEdge::End => caret.bottom(),
};
let knob = Bounds::new(
point(caret.left() - Self::KNOB_SIZE / 2., knob_top),
size(Self::KNOB_SIZE, Self::KNOB_SIZE),
);
window.paint_quad(fill(bar, color));
window.paint_quad(fill(knob, color).corner_radii(Self::KNOB_SIZE / 2.));
}
}
/// The caret line box at `position`, for reporting a selection end.
pub(crate) fn caret_line_box(position: Point<Pixels>, line_height: Pixels) -> Bounds<Pixels> {
Bounds::new(position, size(px(0.), line_height))
}
/// Whether a caret line box shows inside `viewport`: its line overlaps the
/// viewport vertically and its x lies within it. A zero-width box never
/// intersects anything, so this is not `Bounds::intersects`.
pub(crate) fn caret_in_view(caret: Bounds<Pixels>, viewport: Bounds<Pixels>) -> bool {
caret.bottom() > viewport.top()
&& caret.top() < viewport.bottom()
&& caret.left() >= viewport.left()
&& caret.left() <= viewport.right()
}
/// Maps a finger to the text position a handle drag selects.
///
/// The knob a finger holds sits above or below the line, so the finger itself
/// is never over the text it moves. The offset from the finger to the caret
/// box is captured when the drag begins and kept for the rest of it; the
/// mapped point then picks lines exactly as a pointer does, crossing into the
/// line above or below at its edge.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct EdgeDrag {
edge: SelectionEdge,
offset: Point<Pixels>,
}
impl EdgeDrag {
pub(crate) fn begin(edge: SelectionEdge, caret: Bounds<Pixels>, finger: Point<Pixels>) -> Self {
Self {
edge,
offset: caret.center() - finger,
}
}
pub(crate) const fn edge(&self) -> SelectionEdge {
self.edge
}
/// The finger dragged its end past the other one; it now holds that one.
pub(crate) fn set_edge(&mut self, edge: SelectionEdge) {
self.edge = edge;
}
/// The text position the finger points at.
pub(crate) fn text_position(&self, finger: Point<Pixels>) -> Point<Pixels> {
point(finger.x + self.offset.x, finger.y + self.offset.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_reports_bounds_and_edges() {
let start = caret_line_box(point(px(10.), px(20.)), px(16.));
let end = caret_line_box(point(px(80.), px(52.)), px(16.));
let snapshot = TouchSelectionSnapshot::new(start, end)
.with_menu_open(true)
.with_dragging(Some(SelectionEdge::End));
assert_eq!(snapshot.edge(SelectionEdge::Start), start);
assert_eq!(snapshot.edge(SelectionEdge::End), end);
assert!(!snapshot.is_empty());
assert!(snapshot.is_menu_open());
assert_eq!(snapshot.dragging(), Some(SelectionEdge::End));
assert_eq!(
snapshot.bounds(),
Some(Bounds::from_corners(
point(px(10.), px(20.)),
point(px(80.), px(68.))
))
);
let caret = TouchSelectionSnapshot::new(start, start);
assert!(caret.is_empty());
}
#[test]
fn a_scrolled_away_end_keeps_its_geometry_but_no_handle() {
let start = caret_line_box(point(px(10.), px(-30.)), px(16.));
let end = caret_line_box(point(px(80.), px(52.)), px(16.));
let viewport = Bounds::new(point(px(0.), px(0.)), size(px(200.), px(100.)));
assert!(!caret_in_view(start, viewport));
assert!(caret_in_view(end, viewport));
let snapshot =
TouchSelectionSnapshot::new(start, end).with_edge_visible(SelectionEdge::Start, false);
assert!(!snapshot.is_edge_visible(SelectionEdge::Start));
assert_eq!(snapshot.edge(SelectionEdge::Start), start);
assert_eq!(snapshot.bounds(), Some(end));
let gone = snapshot.with_edge_visible(SelectionEdge::End, false);
assert_eq!(gone.bounds(), None);
}
#[test]
fn edge_drag_keeps_the_finger_offset() {
let caret = caret_line_box(point(px(100.), px(40.)), px(20.));
let drag = EdgeDrag::begin(SelectionEdge::End, caret, point(px(102.), px(72.)));
assert_eq!(drag.edge(), SelectionEdge::End);
// The finger started 22px below the caret's center; it stays there.
assert_eq!(
drag.text_position(point(px(150.), px(90.))),
point(px(148.), px(68.))
);
}
}