scroll.rs

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