scroll.rs

  1mod actions;
  2pub(crate) mod autoscroll;
  3pub(crate) mod scroll_amount;
  4
  5use crate::editor_settings::ScrollBeyondLastLine;
  6use crate::{
  7    display_map::{DisplaySnapshot, ToDisplayPoint},
  8    hover_popover::hide_hover,
  9    persistence::DB,
 10    Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings,
 11    InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
 12};
 13pub use autoscroll::{Autoscroll, AutoscrollStrategy};
 14use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
 15use language::{Bias, Point};
 16pub use scroll_amount::ScrollAmount;
 17use settings::Settings;
 18use std::{
 19    cmp::Ordering,
 20    time::{Duration, Instant},
 21};
 22use util::ResultExt;
 23use workspace::{ItemId, WorkspaceId};
 24
 25pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
 26const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 27
 28#[derive(Default)]
 29pub struct ScrollbarAutoHide(pub bool);
 30
 31impl Global for ScrollbarAutoHide {}
 32
 33#[derive(Clone, Copy, Debug, PartialEq)]
 34pub struct ScrollAnchor {
 35    pub offset: gpui::Point<f32>,
 36    pub anchor: Anchor,
 37}
 38
 39impl ScrollAnchor {
 40    fn new() -> Self {
 41        Self {
 42            offset: gpui::Point::default(),
 43            anchor: Anchor::min(),
 44        }
 45    }
 46
 47    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
 48        let mut scroll_position = self.offset;
 49        if self.anchor == Anchor::min() {
 50            scroll_position.y = 0.;
 51        } else {
 52            let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32();
 53            scroll_position.y += scroll_top;
 54        }
 55        scroll_position
 56    }
 57
 58    pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
 59        self.anchor.to_point(buffer).row
 60    }
 61}
 62
 63#[derive(Copy, Clone, PartialEq, Eq, Debug)]
 64pub enum Axis {
 65    Vertical,
 66    Horizontal,
 67}
 68
 69#[derive(Clone, Copy, Debug)]
 70pub struct OngoingScroll {
 71    last_event: Instant,
 72    axis: Option<Axis>,
 73}
 74
 75impl OngoingScroll {
 76    fn new() -> Self {
 77        Self {
 78            last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
 79            axis: None,
 80        }
 81    }
 82
 83    pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
 84        const UNLOCK_PERCENT: f32 = 1.9;
 85        const UNLOCK_LOWER_BOUND: Pixels = px(6.);
 86        let mut axis = self.axis;
 87
 88        let x = delta.x.abs();
 89        let y = delta.y.abs();
 90        let duration = Instant::now().duration_since(self.last_event);
 91        if duration > SCROLL_EVENT_SEPARATION {
 92            //New ongoing scroll will start, determine axis
 93            axis = if x <= y {
 94                Some(Axis::Vertical)
 95            } else {
 96                Some(Axis::Horizontal)
 97            };
 98        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
 99            //Check if the current ongoing will need to unlock
100            match axis {
101                Some(Axis::Vertical) => {
102                    if x > y && x >= y * UNLOCK_PERCENT {
103                        axis = None;
104                    }
105                }
106
107                Some(Axis::Horizontal) => {
108                    if y > x && y >= x * UNLOCK_PERCENT {
109                        axis = None;
110                    }
111                }
112
113                None => {}
114            }
115        }
116
117        match axis {
118            Some(Axis::Vertical) => {
119                *delta = point(px(0.), delta.y);
120            }
121            Some(Axis::Horizontal) => {
122                *delta = point(delta.x, px(0.));
123            }
124            None => {}
125        }
126
127        axis
128    }
129}
130
131pub struct ScrollManager {
132    pub(crate) vertical_scroll_margin: f32,
133    anchor: ScrollAnchor,
134    ongoing: OngoingScroll,
135    autoscroll_request: Option<(Autoscroll, bool)>,
136    last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
137    show_scrollbars: bool,
138    hide_scrollbar_task: Option<Task<()>>,
139    dragging_scrollbar: bool,
140    visible_line_count: Option<f32>,
141    forbid_vertical_scroll: bool,
142}
143
144impl ScrollManager {
145    pub fn new(cx: &mut WindowContext) -> Self {
146        ScrollManager {
147            vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
148            anchor: ScrollAnchor::new(),
149            ongoing: OngoingScroll::new(),
150            autoscroll_request: None,
151            show_scrollbars: true,
152            hide_scrollbar_task: None,
153            dragging_scrollbar: false,
154            last_autoscroll: None,
155            visible_line_count: None,
156            forbid_vertical_scroll: false,
157        }
158    }
159
160    pub fn clone_state(&mut self, other: &Self) {
161        self.anchor = other.anchor;
162        self.ongoing = other.ongoing;
163    }
164
165    pub fn anchor(&self) -> ScrollAnchor {
166        self.anchor
167    }
168
169    pub fn ongoing_scroll(&self) -> OngoingScroll {
170        self.ongoing
171    }
172
173    pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
174        self.ongoing.last_event = Instant::now();
175        self.ongoing.axis = axis;
176    }
177
178    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
179        self.anchor.scroll_position(snapshot)
180    }
181
182    fn set_scroll_position(
183        &mut self,
184        scroll_position: gpui::Point<f32>,
185        map: &DisplaySnapshot,
186        local: bool,
187        autoscroll: bool,
188        workspace_id: Option<WorkspaceId>,
189        cx: &mut ViewContext<Editor>,
190    ) {
191        if self.forbid_vertical_scroll {
192            return;
193        }
194        let (new_anchor, top_row) = if scroll_position.y <= 0. {
195            (
196                ScrollAnchor {
197                    anchor: Anchor::min(),
198                    offset: scroll_position.max(&gpui::Point::default()),
199                },
200                0,
201            )
202        } else {
203            let scroll_top = scroll_position.y;
204            let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
205                ScrollBeyondLastLine::OnePage => scroll_top,
206                ScrollBeyondLastLine::Off => {
207                    if let Some(height_in_lines) = self.visible_line_count {
208                        let max_row = map.max_point().row().0 as f32;
209                        scroll_top.min(max_row - height_in_lines + 1.).max(0.)
210                    } else {
211                        scroll_top
212                    }
213                }
214                ScrollBeyondLastLine::VerticalScrollMargin => {
215                    if let Some(height_in_lines) = self.visible_line_count {
216                        let max_row = map.max_point().row().0 as f32;
217                        scroll_top
218                            .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
219                            .max(0.)
220                    } else {
221                        scroll_top
222                    }
223                }
224            };
225
226            let scroll_top_buffer_point =
227                DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
228            let top_anchor = map
229                .buffer_snapshot
230                .anchor_at(scroll_top_buffer_point, Bias::Right);
231
232            (
233                ScrollAnchor {
234                    anchor: top_anchor,
235                    offset: point(
236                        scroll_position.x.max(0.),
237                        scroll_top - top_anchor.to_display_point(map).row().as_f32(),
238                    ),
239                },
240                scroll_top_buffer_point.row,
241            )
242        };
243
244        self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
245    }
246
247    fn set_anchor(
248        &mut self,
249        anchor: ScrollAnchor,
250        top_row: u32,
251        local: bool,
252        autoscroll: bool,
253        workspace_id: Option<WorkspaceId>,
254        cx: &mut ViewContext<Editor>,
255    ) {
256        if self.forbid_vertical_scroll {
257            return;
258        }
259        self.anchor = anchor;
260        cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
261        self.show_scrollbar(cx);
262        self.autoscroll_request.take();
263        if let Some(workspace_id) = workspace_id {
264            let item_id = cx.view().entity_id().as_u64() as ItemId;
265
266            cx.foreground_executor()
267                .spawn(async move {
268                    DB.save_scroll_position(
269                        item_id,
270                        workspace_id,
271                        top_row,
272                        anchor.offset.x,
273                        anchor.offset.y,
274                    )
275                    .await
276                    .log_err()
277                })
278                .detach()
279        }
280        cx.notify();
281    }
282
283    pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
284        if !self.show_scrollbars {
285            self.show_scrollbars = true;
286            cx.notify();
287        }
288
289        if cx.default_global::<ScrollbarAutoHide>().0 {
290            self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
291                cx.background_executor()
292                    .timer(SCROLLBAR_SHOW_INTERVAL)
293                    .await;
294                editor
295                    .update(&mut cx, |editor, cx| {
296                        editor.scroll_manager.show_scrollbars = false;
297                        cx.notify();
298                    })
299                    .log_err();
300            }));
301        } else {
302            self.hide_scrollbar_task = None;
303        }
304    }
305
306    pub fn scrollbars_visible(&self) -> bool {
307        self.show_scrollbars
308    }
309
310    pub fn autoscroll_request(&self) -> Option<Autoscroll> {
311        self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
312    }
313
314    pub fn is_dragging_scrollbar(&self) -> bool {
315        self.dragging_scrollbar
316    }
317
318    pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
319        if dragging != self.dragging_scrollbar {
320            self.dragging_scrollbar = dragging;
321            cx.notify();
322        }
323    }
324
325    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
326        if max < self.anchor.offset.x {
327            self.anchor.offset.x = max;
328            true
329        } else {
330            false
331        }
332    }
333
334    pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
335        self.forbid_vertical_scroll = forbid;
336    }
337
338    pub fn forbid_vertical_scroll(&self) -> bool {
339        self.forbid_vertical_scroll
340    }
341}
342
343impl Editor {
344    pub fn vertical_scroll_margin(&self) -> usize {
345        self.scroll_manager.vertical_scroll_margin as usize
346    }
347
348    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
349        self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
350        cx.notify();
351    }
352
353    pub fn visible_line_count(&self) -> Option<f32> {
354        self.scroll_manager.visible_line_count
355    }
356
357    pub fn visible_row_count(&self) -> Option<u32> {
358        self.visible_line_count()
359            .map(|line_count| line_count as u32 - 1)
360    }
361
362    pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
363        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
364        self.scroll_manager.visible_line_count = Some(lines);
365        if opened_first_time {
366            cx.spawn(|editor, mut cx| async move {
367                editor
368                    .update(&mut cx, |editor, cx| {
369                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
370                    })
371                    .ok()
372            })
373            .detach()
374        }
375    }
376
377    pub fn apply_scroll_delta(
378        &mut self,
379        scroll_delta: gpui::Point<f32>,
380        cx: &mut ViewContext<Self>,
381    ) {
382        if self.scroll_manager.forbid_vertical_scroll {
383            return;
384        }
385        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
386        let position = self.scroll_manager.anchor.scroll_position(&display_map) + scroll_delta;
387        self.set_scroll_position_taking_display_map(position, true, false, display_map, cx);
388    }
389
390    pub fn set_scroll_position(
391        &mut self,
392        scroll_position: gpui::Point<f32>,
393        cx: &mut ViewContext<Self>,
394    ) {
395        if self.scroll_manager.forbid_vertical_scroll {
396            return;
397        }
398        self.set_scroll_position_internal(scroll_position, true, false, cx);
399    }
400
401    pub(crate) fn set_scroll_position_internal(
402        &mut self,
403        scroll_position: gpui::Point<f32>,
404        local: bool,
405        autoscroll: bool,
406        cx: &mut ViewContext<Self>,
407    ) {
408        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
409        self.set_scroll_position_taking_display_map(scroll_position, local, autoscroll, map, cx);
410    }
411
412    fn set_scroll_position_taking_display_map(
413        &mut self,
414        scroll_position: gpui::Point<f32>,
415        local: bool,
416        autoscroll: bool,
417        display_map: DisplaySnapshot,
418        cx: &mut ViewContext<Self>,
419    ) {
420        hide_hover(self, cx);
421        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
422
423        self.scroll_manager.set_scroll_position(
424            scroll_position,
425            &display_map,
426            local,
427            autoscroll,
428            workspace_id,
429            cx,
430        );
431
432        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
433    }
434
435    pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> gpui::Point<f32> {
436        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
437        self.scroll_manager.anchor.scroll_position(&display_map)
438    }
439
440    pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
441        hide_hover(self, cx);
442        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
443        let top_row = scroll_anchor
444            .anchor
445            .to_point(&self.buffer().read(cx).snapshot(cx))
446            .row;
447        self.scroll_manager
448            .set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
449    }
450
451    pub(crate) fn set_scroll_anchor_remote(
452        &mut self,
453        scroll_anchor: ScrollAnchor,
454        cx: &mut ViewContext<Self>,
455    ) {
456        hide_hover(self, cx);
457        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
458        let snapshot = &self.buffer().read(cx).snapshot(cx);
459        if !scroll_anchor.anchor.is_valid(snapshot) {
460            log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
461            return;
462        }
463        let top_row = scroll_anchor.anchor.to_point(snapshot).row;
464        self.scroll_manager
465            .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
466    }
467
468    pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
469        if matches!(self.mode, EditorMode::SingleLine { .. }) {
470            cx.propagate();
471            return;
472        }
473
474        if self.take_rename(true, cx).is_some() {
475            return;
476        }
477
478        let cur_position = self.scroll_position(cx);
479        let Some(visible_line_count) = self.visible_line_count() else {
480            return;
481        };
482        let new_pos = cur_position + point(0., amount.lines(visible_line_count));
483        self.set_scroll_position(new_pos, cx);
484    }
485
486    /// Returns an ordering. The newest selection is:
487    ///     Ordering::Equal => on screen
488    ///     Ordering::Less => above the screen
489    ///     Ordering::Greater => below the screen
490    pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering {
491        let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
492        let newest_head = self
493            .selections
494            .newest_anchor()
495            .head()
496            .to_display_point(&snapshot);
497        let screen_top = self
498            .scroll_manager
499            .anchor
500            .anchor
501            .to_display_point(&snapshot);
502
503        if screen_top > newest_head {
504            return Ordering::Less;
505        }
506
507        if let Some(visible_lines) = self.visible_line_count() {
508            if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
509                return Ordering::Equal;
510            }
511        }
512
513        Ordering::Greater
514    }
515
516    pub fn read_scroll_position_from_db(
517        &mut self,
518        item_id: u64,
519        workspace_id: WorkspaceId,
520        cx: &mut ViewContext<Editor>,
521    ) {
522        let scroll_position = DB.get_scroll_position(item_id, workspace_id);
523        if let Ok(Some((top_row, x, y))) = scroll_position {
524            let top_anchor = self
525                .buffer()
526                .read(cx)
527                .snapshot(cx)
528                .anchor_at(Point::new(top_row, 0), Bias::Left);
529            let scroll_anchor = ScrollAnchor {
530                offset: gpui::Point::new(x, y),
531                anchor: top_anchor,
532            };
533            self.set_scroll_anchor(scroll_anchor, cx);
534        }
535    }
536}