Skip to main content

editor/editor/
pointer.rs

1//! Presses, drags and clicks below the last block.
2
3use super::*;
4
5/// How often a drag held past the scroll box's edge scrolls it, and the most
6/// one tick moves however far past the pointer is.
7const EDGE_SCROLL_TICK: Duration = Duration::from_millis(16);
8const EDGE_SCROLL_MAX: f32 = 48.0;
9
10impl Editor {
11    /// The task block whose checkbox `at` landed in, in window coordinates.
12    ///
13    /// The box that painted rather than the marker column: a press in the
14    /// gutter beside it is a press on the row, and still places a caret.
15    pub(super) fn checkbox_at(&self, at: gpui::Point<gpui::Pixels>) -> Option<usize> {
16        let ix = self.layouts.block_at(at)?;
17        self.layouts
18            .checkbox_bounds(ix)
19            .is_some_and(|bounds| bounds.contains(&at))
20            .then_some(ix)
21    }
22
23    /// The paragraph a document ending in a fence, a table, a rule or an image
24    /// has no other way to grow: a fence swallows Enter, a cell and a caption
25    /// have nowhere to put one, and a rule holds no caret at all. `false` when
26    /// the last block ends in a body, which can carry on by itself.
27    pub(super) fn append_tail(&mut self, cx: &mut Context<Self>) -> bool {
28        let Some(last) = self.doc.blocks.len().checked_sub(1) else {
29            return false;
30        };
31        if !self.blocks() {
32            return false;
33        }
34        if self.doc.blocks[last].parts().last() == Some(&Part::Body) {
35            return false;
36        }
37        self.edit(EditKind::Structure, cx, |this| {
38            this.doc
39                .edit()
40                .blocks
41                .push(markdown::Block::new(BlockKind::Paragraph(Text::default())));
42            let ix = this.doc.blocks.len() - 1;
43            this.selection = Selection::at(Cursor::new(ix, Part::Body, 0).clamp(&this.doc));
44            vec![]
45        });
46        true
47    }
48
49    /// Press at `position` as if on the document: menus close, the editor
50    /// takes focus, and the caret goes to the nearest place a caret can be —
51    /// below the last block, above the first, or beside a line.
52    ///
53    /// For a host whose own frame around the editor should behave as the page. A
54    /// press the editor's box already took is marked with
55    /// [`Window::prevent_default`], and this ignores one so marked.
56    pub fn press(
57        &mut self,
58        position: gpui::Point<gpui::Pixels>,
59        click_count: usize,
60        modifiers: gpui::Modifiers,
61        window: &mut Window,
62        cx: &mut Context<Self>,
63    ) {
64        if window.default_prevented() {
65            return;
66        }
67        window.prevent_default();
68        self.pressed(position, click_count, modifiers, window, cx);
69    }
70
71    /// Whether a press is being dragged: a selection, a lifted block or an
72    /// image resize.
73    pub(super) fn in_drag(&self) -> bool {
74        self.dragging.is_some()
75            || self.lifted.is_some()
76            || self.resizing.is_some()
77            || self.sizing.is_some()
78            || self.table_drag.is_some()
79    }
80
81    /// Follow a dragged pointer, wherever in the window it is, and scroll the
82    /// host's box while a selection or a lifted block is held past its edge.
83    pub(super) fn drag_to(&mut self, position: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
84        self.drag_at = Some(position);
85        if self.edge_scroll.is_none() && self.edge_step() != 0.0 {
86            self.edge_scroll = Some(cx.spawn(async move |this, cx| {
87                loop {
88                    cx.background_executor().timer(EDGE_SCROLL_TICK).await;
89                    let more = this.update(cx, |this, cx| this.edge_scroll_tick(cx));
90                    if !more.unwrap_or(false) {
91                        return;
92                    }
93                }
94            }));
95        }
96        self.follow(position, cx);
97    }
98
99    /// How far one edge-scroll tick moves the document: negative up, positive
100    /// down, zero while the drag is inside the scroll box or nothing is held.
101    fn edge_step(&self) -> f32 {
102        let (Some(scroll), Some(at)) = (&self.scroll, self.drag_at) else {
103            return 0.0;
104        };
105        if self.dragging.is_none() && self.lifted.is_none() {
106            return 0.0;
107        }
108        let view = scroll.bounds();
109        let past = if at.y < view.top() {
110            f32::from(at.y - view.top())
111        } else if at.y > view.bottom() {
112            f32::from(at.y - view.bottom())
113        } else {
114            return 0.0;
115        };
116        (past / 2.0).clamp(-EDGE_SCROLL_MAX, EDGE_SCROLL_MAX)
117    }
118
119    /// Scroll by one step and follow the held pointer over what scrolled under
120    /// it. Answers whether the drag is still past an edge.
121    fn edge_scroll_tick(&mut self, cx: &mut Context<Self>) -> bool {
122        let step = self.edge_step();
123        let (Some(scroll), Some(at)) = (self.scroll.clone(), self.drag_at) else {
124            return false;
125        };
126        if step == 0.0 {
127            self.edge_scroll = None;
128            return false;
129        }
130        let offset = scroll.offset();
131        let y = (offset.y - gpui::px(step)).clamp(-scroll.max_offset().y, gpui::px(0.0));
132        scroll.set_offset(gpui::point(offset.x, y));
133        self.follow(at, cx);
134        cx.notify();
135        true
136    }
137
138    /// Move whatever is being dragged to `position`.
139    fn follow(&mut self, position: gpui::Point<gpui::Pixels>, cx: &mut Context<Self>) {
140        if self.table_drag.is_some() {
141            return self.drag_table_to(position, cx);
142        }
143        // A lifted block follows the pointer.
144        if let Some((from, _)) = self.lifted {
145            if let Some(to) = self.layouts.block_at(position) {
146                self.lifted = Some((from, to));
147                cx.notify();
148            }
149            return;
150        }
151        if self.drag_height(position.y, cx) {
152            return;
153        }
154        // An image being resized follows the pointer the same way — the
155        // document holds nothing until the handle is released.
156        if let Some((ix, _)) = self.resizing {
157            if let Some(width) = self.dragged_width(ix, position.x) {
158                self.resizing = Some((ix, Some(width)));
159                cx.notify();
160            }
161            return;
162        }
163        if let Some((unit, pressed)) = self.dragging.clone()
164            && let Some((hit, _)) = self.layouts.hit(position)
165        {
166            let (anchor, head) = ui::input::drag_selection(pressed, hit.span(unit, &self.doc));
167            self.selection = Selection::new(anchor, head).clamp(&self.doc);
168            cx.notify();
169        }
170    }
171
172    pub(super) fn pressed(
173        &mut self,
174        position: gpui::Point<gpui::Pixels>,
175        click_count: usize,
176        modifiers: gpui::Modifiers,
177        window: &mut Window,
178        cx: &mut Context<Self>,
179    ) {
180        ui::popover::close_popup(self, cx, |this| &mut this.block_menu);
181        ui::popover::close_popup(self, cx, |this| &mut this.language_menu);
182        ui::popover::close_popup(self, cx, |this| &mut this.table_menu);
183        ui::popover::close_popup(self, cx, |this| &mut this.text_menu);
184        ui::popover::close_popup(self, cx, |this| &mut this.image_menu);
185        self.pasted = None;
186        self.focus_handle.clone().focus(window, cx);
187        // Ahead of the hit test, and returning without one: the
188        // box is a control, and a caret dropped into the row
189        // behind it would move the caret on every check.
190        if let Some(ix) = self.checkbox_at(position) {
191            self.toggle_task(ix, cx);
192            return;
193        }
194        if self.tail_click(position, cx) {
195            return;
196        }
197        let Some((hit, affinity)) = self.layouts.hit(position) else {
198            return cx.notify();
199        };
200        let unit = ui::input::Granularity::of_clicks(click_count);
201        // Shift extends from wherever the anchor already is, which is what
202        // makes click-then-shift-click a range.
203        self.selection = match modifiers.shift {
204            true => self.selection.extend_to(hit).with_affinity(affinity),
205            false => {
206                let span = hit.span(unit, &self.doc);
207                let affinity = match span.end == hit {
208                    true => affinity,
209                    false => Affinity::Downstream,
210                };
211                Selection::new(span.start, span.end).with_affinity(affinity)
212            }
213        }
214        .clamp(&self.doc);
215        self.dragging = (!modifiers.shift).then(|| {
216            let (start, end) = self.selection.ordered();
217            (unit, start..end)
218        });
219        self.run.interrupt();
220        self.caret_moved();
221        // Only the editor sees the press, so only the editor can
222        // say which anchor it landed on.
223        if let Some(id) = self.anchor_at(position) {
224            cx.emit(EditorEvent::AnchorActivated(id));
225        }
226        cx.notify();
227    }
228
229    /// A right press: the picture's menu on a picture; on text, the caret to
230    /// it unless it lands in the selection, and the edit menu at it.
231    pub(super) fn right_pressed(
232        &mut self,
233        position: gpui::Point<gpui::Pixels>,
234        window: &mut Window,
235        cx: &mut Context<Self>,
236    ) {
237        ui::popover::close_popup(self, cx, |this| &mut this.block_menu);
238        ui::popover::close_popup(self, cx, |this| &mut this.language_menu);
239        if let Some(target) = self.image_target_at(position) {
240            self.focus_handle.clone().focus(window, cx);
241            self.image_menu.open(Dropped::new(target, position));
242            return cx.notify();
243        }
244        if let Some((hit, affinity)) = self.layouts.hit(position) {
245            let (start, end) = self.selection.ordered();
246            if self.selection.is_collapsed() || hit < start || hit > end {
247                self.selection = Selection::at(hit).with_affinity(affinity).clamp(&self.doc);
248                self.run.interrupt();
249                self.caret_moved();
250            }
251        }
252        self.focus_handle.clone().focus(window, cx);
253        let items = ui::menu::edit_items(
254            !self.selection.is_collapsed(),
255            cx.read_from_clipboard().is_some(),
256            [&Cut, &Copy, &Paste, &SelectAll],
257            window,
258        );
259        self.text_menu.open(Dropped::new(items, position));
260        cx.notify();
261    }
262
263    /// A click past the end of the document. Without this the document has no
264    /// end: the click snaps back into the block above it, and what gets typed
265    /// lands inside the code the reader was trying to escape.
266    pub(super) fn tail_click(
267        &mut self,
268        at: gpui::Point<gpui::Pixels>,
269        cx: &mut Context<Self>,
270    ) -> bool {
271        let Some(last) = self.doc.blocks.len().checked_sub(1) else {
272            return false;
273        };
274        let Some(bounds) = self.layouts.block_bounds(last) else {
275            return false;
276        };
277        at.y > bounds.origin.y + bounds.size.height && self.append_tail(cx)
278    }
279}