1use std::{cell::RefCell, ops::Range, rc::Rc};
10
11use gpui_kit::base::ScrollbarHandle;
12use gpui_kit::{Bounds, Context, Pixels, Point, ScrollDelta, Size, point, px, size};
13
14use super::{
15 ColumnZone, KeyOf, LiveGridDelegate, LiveGridState, frame::PaintedLine, selection::successor,
16};
17use crate::{
18 engine::{GroupPath, Projection},
19 model::{ColumnIx, ColumnSet},
20 store::RowId,
21};
22
23pub(super) fn page_size(rows_height: Pixels, row_height: Pixels) -> usize {
26 if row_height <= px(0.) {
27 return 1;
28 }
29 ((rows_height / row_height).floor().max(0.) as usize).max(1)
30}
31
32fn offset_from_position(row: usize, offset: Pixels, row_height: Pixels) -> Pixels {
35 -(row_height * row as f32 + offset)
36}
37
38fn position_from_offset(offset: Pixels, row_height: Pixels, max_row: usize) -> (usize, Pixels) {
42 let y = (-offset).max(px(0.));
43 if row_height <= px(0.) {
44 return (0, px(0.));
45 }
46 let row = (y / row_height).floor() as usize;
47 if row >= max_row {
48 return (max_row, px(0.));
49 }
50 (row, (y - row_height * row as f32).max(px(0.)))
51}
52
53#[derive(Default)]
54struct ScrollHandleState {
55 offset: Point<Pixels>,
57 content: Size<Pixels>,
58 viewport: Bounds<Pixels>,
59 requested: Option<Point<Pixels>>,
61}
62
63#[derive(Clone, Default)]
68pub(super) struct GridScrollHandle(Rc<RefCell<ScrollHandleState>>);
69
70impl GridScrollHandle {
71 fn publish(&self, offset: Point<Pixels>, content: Size<Pixels>, viewport: Bounds<Pixels>) {
73 let mut state = self.0.borrow_mut();
74 state.offset = offset;
75 state.content = content;
76 state.viewport = viewport;
77 }
78
79 fn take_request(&self) -> Option<Point<Pixels>> {
80 self.0.borrow_mut().requested.take()
81 }
82}
83
84impl ScrollbarHandle for GridScrollHandle {
85 fn viewport_bounds(&self) -> Bounds<Pixels> {
86 self.0.borrow().viewport
87 }
88
89 fn offset(&self) -> Point<Pixels> {
90 let state = self.0.borrow();
91 state.requested.unwrap_or(state.offset)
92 }
93
94 fn set_offset(&self, offset: Point<Pixels>) {
95 self.0.borrow_mut().requested = Some(offset);
96 }
97
98 fn content_size(&self) -> Size<Pixels> {
99 self.0.borrow().content
100 }
101}
102
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
105pub(super) enum ScrollReset {
106 #[default]
108 Keep,
109 NewView,
112 NewStructure,
115}
116
117impl ScrollReset {
118 pub(super) fn is_reset(self) -> bool {
120 self != Self::Keep
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Eq)]
126pub(super) enum AnchorLine {
127 Row(RowId),
128 Group(GroupPath),
129}
130
131pub(super) struct LineAnchor {
135 line: AnchorLine,
136 screen: usize,
137 focused: bool,
138}
139
140impl LineAnchor {
141 pub(super) fn new(line: AnchorLine, screen: usize, focused: bool) -> Self {
144 Self {
145 line,
146 screen,
147 focused,
148 }
149 }
150}
151
152pub(super) struct ScrollAnchor {
158 columns: Vec<(ColumnIx, Pixels)>,
159 at_start: bool,
160}
161
162impl<D: LiveGridDelegate> LiveGridState<D> {
163 pub fn visible_rows(&self) -> Range<usize> {
166 let count =
167 ((self.rows_area_height() + self.scroll_offset) / self.row_height).ceil() as usize;
168 self.scroll_row..(self.scroll_row + count).min(self.projection.len())
169 }
170
171 pub fn scroll_to_key(&mut self, key: &KeyOf<D>, cx: &mut Context<Self>) -> bool {
176 let Some(display_ix) = self
177 .snapshot
178 .row_id(key)
179 .and_then(|id| self.projection.display_ix(id))
180 else {
181 return false;
182 };
183 self.reveal_row(display_ix);
184 cx.notify();
185 true
186 }
187
188 pub(super) fn reveal_row(&mut self, display_ix: usize) {
190 let viewport_rows = page_size(self.rows_area_height(), self.row_height);
191 if display_ix < self.scroll_row
192 || (display_ix == self.scroll_row && self.scroll_offset > px(0.))
193 {
194 self.scroll_row = display_ix;
195 self.scroll_offset = px(0.);
196 } else if display_ix >= self.scroll_row + viewport_rows {
197 self.scroll_row = display_ix + 1 - viewport_rows;
198 self.scroll_offset = px(0.);
199 }
200 self.clamp_scroll();
201 }
202
203 pub(super) fn rows_area_height(&self) -> Pixels {
206 self.rows_bounds().size.height
207 }
208
209 pub fn scroll_to_row(&mut self, display_ix: usize, cx: &mut Context<Self>) {
211 self.scroll_row = display_ix;
212 self.scroll_offset = px(0.);
213 self.clamp_scroll();
214 cx.notify();
215 }
216
217 pub fn scroll_by(&mut self, delta: Point<Pixels>, cx: &mut Context<Self>) {
219 self.scroll_x = (self.scroll_x + delta.x).max(px(0.));
220 self.scroll_rows_by(delta.y);
221 cx.notify();
222 }
223
224 pub(crate) fn on_scroll_wheel(&mut self, delta: ScrollDelta, cx: &mut Context<Self>) {
228 let delta = delta.pixel_delta(self.row_height);
229 self.scroll_by(point(-delta.x, -delta.y), cx);
230 }
231
232 pub(super) fn scroll_rows_by(&mut self, delta: Pixels) {
234 let mut offset = self.scroll_offset + delta;
237 while offset < px(0.) && self.scroll_row > 0 {
238 self.scroll_row -= 1;
239 offset += self.row_height;
240 }
241 let rows = (offset / self.row_height).floor() as usize;
242 self.scroll_row += rows;
243 self.scroll_offset = (offset - self.row_height * rows as f32).max(px(0.));
244 self.clamp_scroll();
245 }
246
247 pub(super) fn apply_scroll_request(&mut self) {
249 let Some(offset) = self.scroll_handle.take_request() else {
250 return;
251 };
252 let (row, row_offset) =
253 position_from_offset(offset.y, self.row_height, self.max_scroll_row());
254 self.scroll_row = row;
255 self.scroll_offset = row_offset;
256 self.scroll_x = (-offset.x).max(px(0.));
257 self.clamp_scroll();
258 }
259
260 pub(super) fn publish_scroll(&self) {
263 let content = size(
264 self.columns
265 .zone_columns(ColumnZone::Scrolling)
266 .content_width(),
267 self.row_height * self.projection.len() as f32,
268 );
269 let offset = point(
270 -self.scroll_x,
271 offset_from_position(self.scroll_row, self.scroll_offset, self.row_height),
272 );
273 self.scroll_handle
274 .publish(offset, content, self.rows_bounds());
275 }
276
277 fn max_scroll_row(&self) -> usize {
279 let viewport_rows = (self.rows_area_height() / self.row_height).floor() as usize;
280 self.projection.len().saturating_sub(viewport_rows.max(1))
281 }
282
283 pub(super) fn line_anchor(&self) -> Option<LineAnchor> {
289 let anchor = |screen: usize, focused: bool| {
290 let line = match &self.painted[screen] {
291 PaintedLine::Row(id) => AnchorLine::Row(*id),
292 PaintedLine::Caption(path) => AnchorLine::Group(path.clone()),
293 PaintedLine::Footer(_) => return None,
294 PaintedLine::Ghost(_) => return None,
296 };
297 Some(LineAnchor::new(line, screen, focused))
298 };
299 let focused = self.selection.focus().and_then(|focus| {
300 let screen = match (focus.row(), focus.group()) {
301 (Some(key), _) => {
302 let id = self.snapshot.row_id(key)?;
303 self.painted
304 .iter()
305 .position(|painted| painted.row() == Some(id))?
306 }
307 (None, Some(path)) => self
308 .painted
309 .iter()
310 .position(|painted| painted.group() == Some(path))?,
311 (None, None) => return None,
312 };
313 anchor(screen, true)
314 });
315 focused.or_else(|| (0..self.painted.len()).find_map(|screen| anchor(screen, false)))
316 }
317
318 pub(super) fn restore_line_anchor(
330 &mut self,
331 anchor: Option<LineAnchor>,
332 previous: &Projection,
333 scroll_reset: ScrollReset,
334 ) {
335 let shown = |line: &AnchorLine| match line {
336 AnchorLine::Row(id) => self.projection.display_ix(*id),
337 AnchorLine::Group(path) => {
338 let group = self.projection.groups().find(path)?;
339 self.projection.group_line(group.id())
340 }
341 };
342 let at_top = self.scroll_row == 0 && self.scroll_offset == px(0.);
343 if scroll_reset == ScrollReset::NewStructure {
344 match anchor
348 .filter(|anchor| anchor.focused && !at_top)
349 .and_then(|anchor| {
350 shown(&anchor.line).map(|display_ix| (display_ix, anchor.screen))
351 }) {
352 Some((display_ix, screen)) => self.place_row(display_ix, screen),
353 None => self.scroll_to_top(),
354 }
355 return;
356 }
357 if scroll_reset.is_reset() {
358 match anchor.filter(|anchor| anchor.focused).and_then(|anchor| {
359 shown(&anchor.line).map(|display_ix| (display_ix, anchor.screen))
360 }) {
361 Some((display_ix, screen)) => self.place_row(display_ix, screen),
362 None => self.scroll_to_top(),
363 }
364 return;
365 }
366 let Some(anchor) = anchor else {
367 return;
368 };
369 if at_top && !anchor.focused {
370 return;
371 }
372 let display_ix = shown(&anchor.line).or_else(|| {
373 let AnchorLine::Row(id) = anchor.line else {
374 return None;
375 };
376 let snapshot = &self.snapshot;
377 successor(previous, Some(id), |id| {
378 snapshot
379 .row(id)
380 .and_then(|_| self.projection.display_ix(id))
381 })
382 });
383 if let Some(display_ix) = display_ix {
384 self.place_row(display_ix, anchor.screen);
385 }
386 }
387
388 fn place_row(&mut self, display_ix: usize, line: usize) {
391 if display_ix >= line {
392 self.scroll_row = display_ix - line;
393 } else {
394 self.scroll_row = 0;
395 self.scroll_offset = px(0.);
396 }
397 }
398
399 pub(super) fn scroll_to_top(&mut self) {
403 self.scroll_row = 0;
404 self.scroll_offset = px(0.);
405 }
406
407 pub fn scroll_x(&self) -> Pixels {
410 self.scroll_x
411 }
412
413 pub fn set_scroll_x(&mut self, scroll_x: Pixels, cx: &mut Context<Self>) {
415 self.scroll_x = scroll_x.max(px(0.));
416 self.clamp_scroll();
417 cx.notify();
418 }
419
420 pub(super) fn clamp_scroll(&mut self) {
421 let max_row = self.max_scroll_row();
422 if self.scroll_row >= max_row {
423 self.scroll_row = max_row;
424 self.scroll_offset = px(0.);
425 }
426 let content = self
428 .columns
429 .zone_columns(ColumnZone::Scrolling)
430 .content_width();
431 let max_x = (content - self.scrolling_zone_width()).max(px(0.));
432 self.scroll_x = self.scroll_x.min(max_x).max(px(0.));
433 }
434
435 pub(super) fn scrolling_zone_width(&self) -> Pixels {
437 self.columns
438 .geometry(self.body_bounds.size.width, self.scroll_x)
439 .zone(ColumnZone::Scrolling)
440 .width
441 }
442
443 pub(super) fn scroll_anchor(&self) -> ScrollAnchor {
444 let zone = self.columns.zone_columns(ColumnZone::Scrolling);
445 let first = zone.offsets[1..].partition_point(|&end| end <= self.scroll_x);
446 ScrollAnchor {
447 columns: (first..zone.len())
448 .map(|i| (zone.columns[i], zone.offsets[i] - self.scroll_x))
449 .collect(),
450 at_start: self.scroll_x <= px(0.),
451 }
452 }
453
454 pub(super) fn restore_scroll_anchor(&mut self, anchor: ScrollAnchor) {
458 if anchor.at_start {
459 self.scroll_x = px(0.);
460 return;
461 }
462 let zone = self.columns.zone_columns(ColumnZone::Scrolling);
463 for (column, screen_x) in anchor.columns {
464 if let Some((ColumnZone::Scrolling, i)) = self.columns.zone_position(column) {
465 self.scroll_x = (zone.offsets[i] - screen_x).max(px(0.));
466 return;
467 }
468 }
469 }
470
471 pub(super) fn reveal_columns(&mut self, columns: ColumnSet) {
475 let width = self.scrolling_zone_width();
476 if width <= px(0.) {
477 return;
478 }
479 let zone = self.columns.zone_columns(ColumnZone::Scrolling);
480 let mut span: Option<(Pixels, Pixels)> = None;
481 for (i, column) in zone.columns.iter().enumerate() {
482 if columns.contains(*column) {
483 let (left, right) = (zone.offsets[i], zone.offsets[i + 1]);
484 span = Some(span.map_or((left, right), |(l, r)| (l.min(left), r.max(right))));
485 }
486 }
487 let Some((left, right)) = span else {
488 return;
489 };
490 if left < self.scroll_x {
491 self.scroll_x = left;
492 } else if right > self.scroll_x + width {
493 self.scroll_x = (right - width).min(left);
494 }
495 }
496
497 pub(super) fn reveal_right_edge(&mut self, column: ColumnIx) {
500 let width = self.scrolling_zone_width();
501 let Some((ColumnZone::Scrolling, i)) = self.columns.zone_position(column) else {
502 return;
503 };
504 let right = self.columns.zone_columns(ColumnZone::Scrolling).offsets[i + 1];
505 if width > px(0.) && right > self.scroll_x + width {
506 self.scroll_x = right - width;
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use gpui_kit::base::ScrollbarHandle as _;
514 use gpui_kit::{point, px};
515
516 use super::{GridScrollHandle, offset_from_position, page_size, position_from_offset};
517
518 #[test]
519 fn test_page_size() {
520 for (row_height, page) in [(26., 10), (30., 8), (32., 8), (40., 6)] {
522 assert_eq!(page_size(px(260.), px(row_height)), page, "{row_height} px");
523 }
524 assert_eq!(page_size(px(259.), px(26.)), 9);
526 assert_eq!(page_size(px(0.), px(26.)), 1);
528 assert_eq!(page_size(px(10.), px(26.)), 1);
529 assert_eq!(page_size(px(260.), px(0.)), 1);
530 }
531
532 #[test]
533 fn test_grid_scroll_handle() {
534 let row = px(26.);
535 assert_eq!(offset_from_position(0, px(0.), row), px(0.));
537 assert_eq!(offset_from_position(3, px(5.), row), px(-83.));
538 for (first, inside) in [(0, 0.), (3, 5.), (12_345, 13.5), (99_990, 0.)] {
540 let offset = offset_from_position(first, px(inside), row);
541 let (back, back_inside) = position_from_offset(offset, row, 99_990);
542 assert_eq!(back, first);
543 assert!(
544 (back_inside - px(inside)).abs() < px(0.5),
545 "{first}: {back_inside:?}"
546 );
547 }
548 assert_eq!(
550 position_from_offset(px(-26. * 200.), row, 100),
551 (100, px(0.))
552 );
553 assert_eq!(position_from_offset(px(40.), row, 100), (0, px(0.)));
554
555 let handle = GridScrollHandle::default();
557 handle.publish(
558 point(px(0.), px(-52.)),
559 gpui_kit::size(px(500.), px(2_600.)),
560 Default::default(),
561 );
562 assert_eq!(handle.offset(), point(px(0.), px(-52.)));
563 assert_eq!(handle.content_size().height, px(2_600.));
564 handle.set_offset(point(px(-10.), px(-104.)));
565 assert_eq!(handle.offset(), point(px(-10.), px(-104.)));
566 assert_eq!(handle.take_request(), Some(point(px(-10.), px(-104.))));
567 assert_eq!(handle.take_request(), None);
568 assert_eq!(handle.offset(), point(px(0.), px(-52.)));
569 }
570}