1use gpui::{Bounds, Hsla, Pixels, Point, Window, fill, point, px, size};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum SelectionEdge {
17 Start,
19 End,
21}
22
23impl SelectionEdge {
24 pub const fn opposite(self) -> Self {
26 match self {
27 Self::Start => Self::End,
28 Self::End => Self::Start,
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct TouchSelectionSnapshot {
42 start: Bounds<Pixels>,
43 end: Bounds<Pixels>,
44 start_visible: bool,
48 end_visible: bool,
49 menu_open: bool,
50 dragging: Option<SelectionEdge>,
51}
52
53impl TouchSelectionSnapshot {
54 pub(crate) const fn new(start: Bounds<Pixels>, end: Bounds<Pixels>) -> Self {
55 Self {
56 start,
57 end,
58 start_visible: true,
59 end_visible: true,
60 menu_open: false,
61 dragging: None,
62 }
63 }
64
65 pub(crate) const fn with_edge_visible(mut self, edge: SelectionEdge, visible: bool) -> Self {
66 match edge {
67 SelectionEdge::Start => self.start_visible = visible,
68 SelectionEdge::End => self.end_visible = visible,
69 }
70 self
71 }
72
73 pub(crate) const fn with_menu_open(mut self, menu_open: bool) -> Self {
74 self.menu_open = menu_open;
75 self
76 }
77
78 pub(crate) const fn with_dragging(mut self, dragging: Option<SelectionEdge>) -> Self {
79 self.dragging = dragging;
80 self
81 }
82
83 pub const fn start(&self) -> Bounds<Pixels> {
85 self.start
86 }
87
88 pub const fn end(&self) -> Bounds<Pixels> {
90 self.end
91 }
92
93 pub const fn edge(&self, edge: SelectionEdge) -> Bounds<Pixels> {
95 match edge {
96 SelectionEdge::Start => self.start,
97 SelectionEdge::End => self.end,
98 }
99 }
100
101 pub const fn is_edge_visible(&self, edge: SelectionEdge) -> bool {
104 match edge {
105 SelectionEdge::Start => self.start_visible,
106 SelectionEdge::End => self.end_visible,
107 }
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.start == self.end
113 }
114
115 pub fn bounds(&self) -> Option<Bounds<Pixels>> {
118 match (self.start_visible, self.end_visible) {
119 (true, true) => Some(self.start.union(&self.end)),
120 (true, false) => Some(self.start),
121 (false, true) => Some(self.end),
122 (false, false) => None,
123 }
124 }
125
126 pub const fn is_menu_open(&self) -> bool {
129 self.menu_open
130 }
131
132 pub const fn dragging(&self) -> Option<SelectionEdge> {
134 self.dragging
135 }
136}
137
138pub struct TouchHandle;
146
147impl TouchHandle {
148 pub const HIT_SIZE: Pixels = px(44.);
150 pub const KNOB_SIZE: Pixels = px(10.);
152 pub const BAR_WIDTH: Pixels = px(2.);
154 pub const EXTENT: Pixels = px(12.);
156
157 pub fn hit_bounds(edge: SelectionEdge, caret: Bounds<Pixels>) -> Bounds<Pixels> {
159 let top = match edge {
160 SelectionEdge::Start => caret.top() - Self::EXTENT,
161 SelectionEdge::End => caret.top(),
162 };
163 Bounds::new(
164 point(caret.left() - Self::HIT_SIZE / 2., top),
165 size(Self::HIT_SIZE, caret.size.height + Self::EXTENT),
166 )
167 }
168
169 pub fn paint(edge: SelectionEdge, caret: Bounds<Pixels>, color: Hsla, window: &mut Window) {
171 let bar = Bounds::new(
172 point(caret.left() - Self::BAR_WIDTH / 2., caret.top()),
173 size(Self::BAR_WIDTH, caret.size.height),
174 );
175 let knob_top = match edge {
176 SelectionEdge::Start => caret.top() - Self::KNOB_SIZE,
177 SelectionEdge::End => caret.bottom(),
178 };
179 let knob = Bounds::new(
180 point(caret.left() - Self::KNOB_SIZE / 2., knob_top),
181 size(Self::KNOB_SIZE, Self::KNOB_SIZE),
182 );
183 window.paint_quad(fill(bar, color));
184 window.paint_quad(fill(knob, color).corner_radii(Self::KNOB_SIZE / 2.));
185 }
186}
187
188pub(crate) fn caret_line_box(position: Point<Pixels>, line_height: Pixels) -> Bounds<Pixels> {
190 Bounds::new(position, size(px(0.), line_height))
191}
192
193pub(crate) fn caret_in_view(caret: Bounds<Pixels>, viewport: Bounds<Pixels>) -> bool {
197 caret.bottom() > viewport.top()
198 && caret.top() < viewport.bottom()
199 && caret.left() >= viewport.left()
200 && caret.left() <= viewport.right()
201}
202
203#[derive(Clone, Copy, Debug, PartialEq)]
211pub(crate) struct EdgeDrag {
212 edge: SelectionEdge,
213 offset: Point<Pixels>,
214}
215
216impl EdgeDrag {
217 pub(crate) fn begin(edge: SelectionEdge, caret: Bounds<Pixels>, finger: Point<Pixels>) -> Self {
218 Self {
219 edge,
220 offset: caret.center() - finger,
221 }
222 }
223
224 pub(crate) const fn edge(&self) -> SelectionEdge {
225 self.edge
226 }
227
228 pub(crate) fn set_edge(&mut self, edge: SelectionEdge) {
230 self.edge = edge;
231 }
232
233 pub(crate) fn text_position(&self, finger: Point<Pixels>) -> Point<Pixels> {
235 point(finger.x + self.offset.x, finger.y + self.offset.y)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn snapshot_reports_bounds_and_edges() {
245 let start = caret_line_box(point(px(10.), px(20.)), px(16.));
246 let end = caret_line_box(point(px(80.), px(52.)), px(16.));
247 let snapshot = TouchSelectionSnapshot::new(start, end)
248 .with_menu_open(true)
249 .with_dragging(Some(SelectionEdge::End));
250
251 assert_eq!(snapshot.edge(SelectionEdge::Start), start);
252 assert_eq!(snapshot.edge(SelectionEdge::End), end);
253 assert!(!snapshot.is_empty());
254 assert!(snapshot.is_menu_open());
255 assert_eq!(snapshot.dragging(), Some(SelectionEdge::End));
256 assert_eq!(
257 snapshot.bounds(),
258 Some(Bounds::from_corners(
259 point(px(10.), px(20.)),
260 point(px(80.), px(68.))
261 ))
262 );
263
264 let caret = TouchSelectionSnapshot::new(start, start);
265 assert!(caret.is_empty());
266 }
267
268 #[test]
269 fn a_scrolled_away_end_keeps_its_geometry_but_no_handle() {
270 let start = caret_line_box(point(px(10.), px(-30.)), px(16.));
271 let end = caret_line_box(point(px(80.), px(52.)), px(16.));
272 let viewport = Bounds::new(point(px(0.), px(0.)), size(px(200.), px(100.)));
273 assert!(!caret_in_view(start, viewport));
274 assert!(caret_in_view(end, viewport));
275
276 let snapshot =
277 TouchSelectionSnapshot::new(start, end).with_edge_visible(SelectionEdge::Start, false);
278 assert!(!snapshot.is_edge_visible(SelectionEdge::Start));
279 assert_eq!(snapshot.edge(SelectionEdge::Start), start);
280 assert_eq!(snapshot.bounds(), Some(end));
281
282 let gone = snapshot.with_edge_visible(SelectionEdge::End, false);
283 assert_eq!(gone.bounds(), None);
284 }
285
286 #[test]
287 fn edge_drag_keeps_the_finger_offset() {
288 let caret = caret_line_box(point(px(100.), px(40.)), px(20.));
289 let drag = EdgeDrag::begin(SelectionEdge::End, caret, point(px(102.), px(72.)));
290 assert_eq!(drag.edge(), SelectionEdge::End);
291 assert_eq!(
293 drag.text_position(point(px(150.), px(90.))),
294 point(px(148.), px(68.))
295 );
296 }
297}