diff --git a/Cargo.lock b/Cargo.lock index 74a098e7..d94b341f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -733,7 +733,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.11.0", "lazy_static", "lazycell", "log", @@ -756,7 +756,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.11.0", "log", "prettyplease", "proc-macro2", @@ -1432,7 +1432,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "indexmap 2.9.0", "rustc-hash 2.1.0", @@ -1990,7 +1990,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "proc-macro2", "quote", @@ -3142,7 +3142,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -3327,7 +3327,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3597,7 +3597,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "bytes", @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "rustls", "rustls-platform-verifier", @@ -4441,7 +4441,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "derive_refineable", "workspace-hack", @@ -6545,7 +6545,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "bytes", @@ -7121,7 +7121,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "serde", @@ -7704,7 +7704,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "arrayvec", "log", @@ -8931,7 +8931,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#65759d43163bd36cce8f8aa492ba2628ca005a7a" +source = "git+https://github.com/zed-industries/zed.git#5488398986487afa6e5b38ea55f768271117b18b" dependencies = [ "anyhow", "async-fs 2.1.2", diff --git a/crates/story/src/virtual_list_story.rs b/crates/story/src/virtual_list_story.rs index d978c8a3..fd95ff94 100644 --- a/crates/story/src/virtual_list_story.rs +++ b/crates/story/src/virtual_list_story.rs @@ -2,7 +2,7 @@ use std::{ops::Range, rc::Rc}; use gpui::{ div, px, size, App, AppContext, Context, Div, Entity, FocusHandle, Focusable, - InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollHandle, Size, Styled, + InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollStrategy, Size, Styled, Window, }; use gpui_component::{ @@ -10,12 +10,12 @@ use gpui_component::{ divider::Divider, h_flex, scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}, - v_flex, v_virtual_list, ActiveTheme as _, Selectable, + v_flex, v_virtual_list, ActiveTheme as _, Selectable, Sizable, VirtualListScrollHandle, }; pub struct VirtualListStory { focus_handle: FocusHandle, - scroll_handle: ScrollHandle, + scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, items: Vec, item_sizes: Rc>>, @@ -34,7 +34,7 @@ impl VirtualListStory { Self { focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), + scroll_handle: VirtualListScrollHandle::new(), scroll_state: ScrollbarState::default(), items, item_sizes: Rc::new(item_sizes), @@ -79,80 +79,125 @@ impl VirtualListStory { } fn render_buttons(&mut self, cx: &mut Context) -> impl IntoElement { - h_flex() + v_flex() .gap_2() - .justify_between() + .child( + h_flex() + .gap_2() + .justify_between() + .child( + h_flex() + .gap_2() + .child( + ButtonGroup::new("test-cases") + .outline() + .compact() + .child( + Button::new("test-0") + .label("Size 0") + .selected(self.size_mode == 0), + ) + .child( + Button::new("test-1") + .label("Size 1") + .selected(self.size_mode == 1), + ) + .child( + Button::new("test-2") + .label("Size 2") + .selected(self.size_mode == 2), + ) + .child( + Button::new("test-3") + .label("Size 3") + .selected(self.size_mode == 3), + ) + .on_click(cx.listener(|view, clicks: &Vec, _, cx| { + if clicks.contains(&0) { + view.change_test_cases(0, cx) + } else if clicks.contains(&1) { + view.change_test_cases(1, cx) + } else if clicks.contains(&2) { + view.change_test_cases(2, cx) + } else if clicks.contains(&3) { + view.change_test_cases(3, cx) + } + })), + ) + .child(Divider::vertical().px_2()) + .child( + ButtonGroup::new("scrollbars") + .outline() + .compact() + .child( + Button::new("test-axis-both") + .label("Both Scrollbar") + .selected(self.axis.is_both()), + ) + .child( + Button::new("test-axis-vertical") + .label("Vertical") + .selected(self.axis.is_vertical()), + ) + .child( + Button::new("test-axis-horizontal") + .label("Horizontal") + .selected(self.axis.is_horizontal()), + ) + .on_click(cx.listener(|view, clicks: &Vec, _, cx| { + if clicks.contains(&0) { + view.change_axis(ScrollbarAxis::Both, cx) + } else if clicks.contains(&1) { + view.change_axis(ScrollbarAxis::Vertical, cx) + } else if clicks.contains(&2) { + view.change_axis(ScrollbarAxis::Horizontal, cx) + } + })), + ), + ) + .child(format!("visible_range: {:?}", self.visible_range)), + ) .child( h_flex() .gap_2() .child( - ButtonGroup::new("test-cases") + Button::new("scroll-to0") + .small() .outline() - .compact() - .child( - Button::new("test-0") - .label("Size 0") - .selected(self.size_mode == 0), - ) - .child( - Button::new("test-1") - .label("Size 1") - .selected(self.size_mode == 1), - ) - .child( - Button::new("test-2") - .label("Size 2") - .selected(self.size_mode == 2), - ) - .child( - Button::new("test-3") - .label("Size 3") - .selected(self.size_mode == 3), - ) - .on_click(cx.listener(|view, clicks: &Vec, _, cx| { - if clicks.contains(&0) { - view.change_test_cases(0, cx) - } else if clicks.contains(&1) { - view.change_test_cases(1, cx) - } else if clicks.contains(&2) { - view.change_test_cases(2, cx) - } else if clicks.contains(&3) { - view.change_test_cases(3, cx) - } + .label("Scroll to Top") + .on_click(cx.listener(|this, _, _, _| { + this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); })), ) - .child(Divider::vertical().px_2()) .child( - ButtonGroup::new("scrollbars") + Button::new("scroll-to1") + .small() .outline() - .compact() - .child( - Button::new("test-axis-both") - .label("Both Scrollbar") - .selected(self.axis.is_both()), - ) - .child( - Button::new("test-axis-vertical") - .label("Vertical") - .selected(self.axis.is_vertical()), - ) - .child( - Button::new("test-axis-horizontal") - .label("Horizontal") - .selected(self.axis.is_horizontal()), - ) - .on_click(cx.listener(|view, clicks: &Vec, _, cx| { - if clicks.contains(&0) { - view.change_axis(ScrollbarAxis::Both, cx) - } else if clicks.contains(&1) { - view.change_axis(ScrollbarAxis::Vertical, cx) - } else if clicks.contains(&2) { - view.change_axis(ScrollbarAxis::Horizontal, cx) - } + .label("Scroll to 50") + .on_click(cx.listener(|this, _, _, _| { + this.scroll_handle.scroll_to_item(50, ScrollStrategy::Top); + })), + ) + .child( + Button::new("scroll-to2") + .small() + .outline() + .label("Scroll to 25 (center)") + .on_click(cx.listener(|this, _, _, _| { + this.scroll_handle + .scroll_to_item(25, ScrollStrategy::Center); + })), + ) + .child( + Button::new("scroll-to-bottom") + .small() + .outline() + .label("Scroll to Bottom") + .on_click(cx.listener(|this, _, _, _| { + this.scroll_handle.scroll_to_bottom(); })), ), ) - .child(format!("visible_range: {:?}", self.visible_range)) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 92430d5d..0eb20cb1 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -76,7 +76,7 @@ pub use root::{ContextModal, Root}; pub use styled::*; pub use time::*; pub use title_bar::*; -pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList}; +pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList, VirtualListScrollHandle}; pub use window_border::{window_border, window_paddings, WindowBorder}; pub use icon::*; diff --git a/crates/ui/src/table/mod.rs b/crates/ui/src/table/mod.rs index 107618b5..a337e3f4 100644 --- a/crates/ui/src/table/mod.rs +++ b/crates/ui/src/table/mod.rs @@ -7,12 +7,13 @@ use crate::{ popup_menu::PopupMenu, scroll::{self, ScrollableMask, Scrollbar, ScrollbarState}, v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt, + VirtualListScrollHandle, }; use gpui::{ actions, canvas, div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Axis, Bounds, Context, Div, DragMoveEvent, Edges, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, - Pixels, Point, Render, ScrollHandle, ScrollStrategy, ScrollWheelEvent, SharedString, + Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, Window, }; @@ -103,7 +104,7 @@ pub struct Table { pub vertical_scroll_handle: UniformListScrollHandle, pub vertical_scroll_state: ScrollbarState, - pub horizontal_scroll_handle: ScrollHandle, + pub horizontal_scroll_handle: VirtualListScrollHandle, pub horizontal_scroll_state: ScrollbarState, scrollbar_visible: Edges, @@ -137,7 +138,7 @@ where focus_handle: cx.focus_handle(), delegate, col_groups: Vec::new(), - horizontal_scroll_handle: ScrollHandle::new(), + horizontal_scroll_handle: VirtualListScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(), vertical_scroll_state: ScrollbarState::default(), horizontal_scroll_state: ScrollbarState::default(), @@ -269,6 +270,17 @@ where cx.notify(); } + fn fixed_left_cols_count(&self) -> usize { + if !self.col_fixed { + return 0; + } + + self.col_groups + .iter() + .filter(|col| col.column.fixed == Some(ColumnFixed::Left)) + .count() + } + /// Scroll to the row at the given index. pub fn scroll_to_row(&mut self, row_ix: usize, cx: &mut Context) { self.vertical_scroll_handle @@ -277,11 +289,13 @@ where } // Scroll to the column at the given index. - // TODO: Fix scroll to selected col, this was not working after fixed col. - // pub fn scroll_to_col(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context) { - // self.horizontal_scroll_handle.scroll_to_item(col_ix); - // cx.notify(); - // } + pub fn scroll_to_col(&mut self, col_ix: usize, cx: &mut Context) { + let col_ix = col_ix.saturating_sub(self.fixed_left_cols_count()); + + self.horizontal_scroll_handle + .scroll_to_item(col_ix, ScrollStrategy::Top); + cx.notify(); + } /// Returns the selected row index. pub fn selected_row(&self) -> Option { @@ -310,11 +324,8 @@ where pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut Context) { self.selection_state = SelectionState::Column; self.selected_col = Some(col_ix); - if let Some(_col_ix) = self.selected_col { - // TODO: Fix scroll to selected col, this was not working after fixed col. - // if self.col_groups[col_ix].fixed.is_none() { - // self.horizontal_scroll_handle.scroll_to_item(col_ix); - // } + if let Some(col_ix) = self.selected_col { + self.scroll_to_col(col_ix, cx); } cx.emit(TableEvent::SelectColumn(col_ix)); cx.notify(); diff --git a/crates/ui/src/virtual_list.rs b/crates/ui/src/virtual_list.rs index 45ce3685..c3c1d978 100644 --- a/crates/ui/src/virtual_list.rs +++ b/crates/ui/src/virtual_list.rs @@ -10,16 +10,147 @@ //! Unlike the `uniform_list`, the each item can have different size. //! //! This is useful for more complex layout, for example, a table with different row height. -use std::{cmp, ops::Range, rc::Rc}; +use std::{ + cell::RefCell, + cmp, + ops::{Deref, Range}, + rc::Rc, +}; use gpui::{ div, point, px, size, Along, AnyElement, App, AvailableSpace, Axis, Bounds, ContentMask, - Context, Div, Element, ElementId, Entity, GlobalElementId, Hitbox, InteractiveElement, - IntoElement, IsZero as _, Pixels, Render, ScrollHandle, Size, Stateful, + Context, Div, Element, ElementId, Entity, GlobalElementId, Half, Hitbox, InteractiveElement, + IntoElement, IsZero as _, Pixels, Point, Render, ScrollHandle, ScrollStrategy, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, }; use smallvec::SmallVec; +use crate::{scroll::ScrollHandleOffsetable, AxisExt}; + +struct VirtualListScrollHandleState { + axis: Axis, + items_bounds: Vec>, +} + +#[derive(Clone)] +pub struct VirtualListScrollHandle { + state: Rc>, + base_handle: ScrollHandle, +} + +impl From for VirtualListScrollHandle { + fn from(handle: ScrollHandle) -> Self { + let mut this = VirtualListScrollHandle::new(); + this.base_handle = handle; + this + } +} + +impl AsRef for VirtualListScrollHandle { + fn as_ref(&self) -> &ScrollHandle { + &self.base_handle + } +} + +impl ScrollHandleOffsetable for VirtualListScrollHandle { + fn offset(&self) -> Point { + self.base_handle.offset() + } + + fn set_offset(&self, offset: Point) { + self.base_handle.set_offset(offset); + } + + fn content_size(&self) -> Size { + self.base_handle.content_size() + } +} + +impl Deref for VirtualListScrollHandle { + type Target = ScrollHandle; + + fn deref(&self) -> &Self::Target { + &self.base_handle + } +} + +impl VirtualListScrollHandle { + pub fn new() -> Self { + VirtualListScrollHandle { + state: Rc::new(RefCell::new(VirtualListScrollHandleState { + axis: Axis::Vertical, + items_bounds: vec![], + })), + base_handle: ScrollHandle::default(), + } + } + + pub fn base_handle(&self) -> &ScrollHandle { + &self.base_handle + } + + fn set_items_bounds(&self, items_bounds: Vec>) { + self.state.borrow_mut().items_bounds = items_bounds; + } + + fn set_axis(&self, axis: Axis) { + self.state.borrow_mut().axis = axis; + } + + /// Scroll to the item at the given index. + pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { + let state = self.state.borrow(); + let Some(bounds) = state.items_bounds.get(ix) else { + return; + }; + + let axis = state.axis; + let mut scroll_offset = self.base_handle.offset(); + let container_bounds = self.base_handle().bounds(); + + match strategy { + ScrollStrategy::Center => { + if axis.is_vertical() { + scroll_offset.y = container_bounds.top() + container_bounds.size.height.half() + - bounds.top() + - bounds.size.height.half() + } else { + scroll_offset.x = container_bounds.left() + container_bounds.size.width.half() + - bounds.left() + - bounds.size.width.half() + } + } + _ => { + // Ref: https://github.com/zed-industries/zed/blob/0d145289e0867a8d5d63e5e1397a5ca69c9d49c3/crates/gpui/src/elements/div.rs#L3026 + if axis.is_vertical() { + if bounds.top() + scroll_offset.y < container_bounds.top() { + scroll_offset.y = container_bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset.y > container_bounds.bottom() { + scroll_offset.y = container_bounds.bottom() - bounds.bottom(); + } + } else { + if bounds.left() + scroll_offset.x < container_bounds.left() { + scroll_offset.x = container_bounds.left() - bounds.left(); + } else if bounds.right() + scroll_offset.x > container_bounds.right() { + scroll_offset.x = container_bounds.right() - bounds.right(); + } + } + } + } + + self.base_handle.set_offset(scroll_offset); + } + + /// Scrolls to the bottom of the list. + pub fn scroll_to_bottom(&self) { + let state = self.state.borrow(); + self.scroll_to_item( + state.items_bounds.len().saturating_sub(1), + ScrollStrategy::Top, + ); + } +} + /// Create a [`VirtualList`] in vertical direction. /// /// This is like `uniform_list` in GPUI, but support two axis. @@ -70,7 +201,7 @@ where V: Render, { let id: ElementId = id.into(); - let scroll_handle = ScrollHandle::default(); + let scroll_handle = VirtualListScrollHandle::new(); let render_range = move |visible_range, window: &mut Window, cx: &mut App| { view.update(cx, |this, cx| { f(this, visible_range, window, cx) @@ -100,7 +231,7 @@ pub struct VirtualList { id: ElementId, axis: Axis, base: Stateful
, - scroll_handle: ScrollHandle, + scroll_handle: VirtualListScrollHandle, items_count: usize, item_sizes: Rc>>, render_items: Box< @@ -115,7 +246,7 @@ impl Styled for VirtualList { } impl VirtualList { - pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + pub fn track_scroll(mut self, scroll_handle: &VirtualListScrollHandle) -> Self { self.base = self.base.track_scroll(&scroll_handle); self.scroll_handle = scroll_handle.clone(); self @@ -124,7 +255,7 @@ impl VirtualList { /// Specify for table. /// /// Table is special, because the `scroll_handle` is based on Table head (That is not a virtual list). - pub(crate) fn with_scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self { + pub(crate) fn with_scroll_handle(mut self, scroll_handle: &VirtualListScrollHandle) -> Self { self.base = div().id(self.id.clone()).size_full(); self.scroll_handle = scroll_handle.clone(); self @@ -311,6 +442,28 @@ impl Element for VirtualList { }, }; + // Update scroll_handle with the item bounds + let items_bounds = item_origins + .iter() + .enumerate() + .map(|(i, &origin)| { + let item_size = item_sizes[i]; + + Bounds { + origin: match self.axis { + Axis::Horizontal => point(bounds.left() + origin + padding.left, px(0.)), + Axis::Vertical => point(px(0.), bounds.top() + origin + padding.top), + }, + size: match self.axis { + Axis::Horizontal => size(item_size, bounds.size.height), + Axis::Vertical => size(bounds.size.width, item_size), + }, + } + }) + .collect::>(); + self.scroll_handle.set_axis(self.axis); + self.scroll_handle.set_items_bounds(items_bounds); + self.base.interactivity().prepaint( global_id, inspector_id,