From 77c7fa53da677e209462eb7a6696b4bb58b01b92 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Aug 2021 12:45:17 -0600 Subject: [PATCH] Introduce Orientation concept to List When the Orientation is Bottom, we paint elements from the bottom of the list when underflowing and express scroll position relative to the bottom. In either orientation, when inserting elements outside the visible area, we adjust the scroll position as needed to keep the visible elements stable. Co-Authored-By: Max Brunsfeld Co-Authored-By: Antonio Scandurra --- gpui/src/elements/list.rs | 69 ++++++++++++++++++++++++++++++++------- zed/src/chat_panel.rs | 3 +- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 78f8c2316e32ffc3ab4679d1461ae78f7967f1c6..b4213fc5a50c825c9ae353dbb6fdef460f4fb170 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -19,11 +19,18 @@ pub struct List { #[derive(Clone)] pub struct ListState(Arc>); +#[derive(Eq, PartialEq)] +pub enum Orientation { + Top, + Bottom, +} + struct StateInner { last_layout_width: f32, elements: Vec, heights: SumTree, - scroll_top: f32, + scroll_position: f32, + orientation: Orientation, } #[derive(Clone, Debug)] @@ -69,6 +76,11 @@ impl Element for List { item_constraint.min.set_y(0.); item_constraint.max.set_y(f32::INFINITY); + let size = constraint.max; + + let visible_top = state.scroll_top(size.y()); + let visible_bottom = visible_top + size.y(); + if state.last_layout_width == constraint.max.x() { let mut old_heights = state.heights.cursor::(); let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &()); @@ -78,6 +90,21 @@ impl Element for List { let size = state.elements[old_heights.sum_start().count].layout(item_constraint, cx); new_heights.push(ElementHeight::Ready(size.y()), &()); + + // Adjust scroll position to keep visible elements stable + match state.orientation { + Orientation::Top => { + if new_heights.summary().height < visible_top { + state.scroll_position += size.y(); + } + } + Orientation::Bottom => { + if new_heights.summary().height - size.y() > visible_bottom { + state.scroll_position += size.y(); + } + } + } + old_heights.next(&()); } else { new_heights.push_tree( @@ -102,7 +129,7 @@ impl Element for List { state.last_layout_width = constraint.max.x(); } - (constraint.max, ()) + (size, ()) } fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) { @@ -115,8 +142,15 @@ impl Element for List { cursor.seek(&Count(visible_range.start), Bias::Right, &()); cursor.sum_start().0 }; + if state.orientation == Orientation::Bottom + && bounds.height() > state.heights.summary().height + { + item_top += bounds.height() - state.heights.summary().height; + } + let scroll_top = state.scroll_top(bounds.height()); + for element in &mut state.elements[visible_range] { - let origin = bounds.origin() + vec2f(0., item_top - state.scroll_top); + let origin = bounds.origin() + vec2f(0., item_top - scroll_top); element.paint(origin, cx); item_top += element.size().y(); } @@ -167,20 +201,21 @@ impl Element for List { json!({ "visible_range": visible_range, "visible_elements": visible_elements, - "scroll_top": state.scroll_top, + "scroll_position": state.scroll_position, }) } } impl ListState { - pub fn new(elements: Vec) -> Self { + pub fn new(elements: Vec, orientation: Orientation) -> Self { let mut heights = SumTree::new(); heights.extend(elements.iter().map(|_| ElementHeight::Pending), &()); Self(Arc::new(Mutex::new(StateInner { last_layout_width: 0., elements, heights, - scroll_top: 0., + scroll_position: 0., + orientation, }))) } @@ -215,9 +250,9 @@ impl ListState { impl StateInner { fn visible_range(&self, height: f32) -> Range { let mut cursor = self.heights.cursor::(); - cursor.seek(&Height(self.scroll_top), Bias::Right, &()); + cursor.seek(&Height(self.scroll_top(height)), Bias::Right, &()); let start_ix = cursor.sum_start().0; - cursor.seek(&Height(self.scroll_top + height), Bias::Left, &()); + cursor.seek(&Height(self.scroll_top(height) + height), Bias::Left, &()); let end_ix = cursor.sum_start().0; start_ix..self.elements.len().min(end_ix + 1) } @@ -235,12 +270,24 @@ impl StateInner { } let scroll_max = (self.heights.summary().height - height).max(0.); - self.scroll_top = (self.scroll_top - delta.y()).max(0.).min(scroll_max); - + let delta_y = match self.orientation { + Orientation::Top => -delta.y(), + Orientation::Bottom => delta.y(), + }; + self.scroll_position = (self.scroll_position + delta_y).max(0.).min(scroll_max); cx.notify(); true } + + fn scroll_top(&self, height: f32) -> f32 { + match self.orientation { + Orientation::Top => self.scroll_position, + Orientation::Bottom => { + (self.heights.summary().height - height - self.scroll_position).max(0.) + } + } + } } impl ElementHeight { @@ -329,7 +376,7 @@ mod tests { fn test_layout(cx: &mut crate::MutableAppContext) { let mut presenter = cx.build_presenter(0, 20.0); let mut layout_cx = presenter.layout_cx(cx); - let state = ListState::new(vec![item(20.), item(30.), item(10.)]); + let state = ListState::new(vec![item(20.), item(30.), item(10.)], Orientation::Top); let mut list = List::new(state.clone()).boxed(); let size = list.layout( diff --git a/zed/src/chat_panel.rs b/zed/src/chat_panel.rs index 30ce44b82909fb61343936dbbdd719925053ec12..0c4fbd584f44d530b6e29a1335b06bf820a0a418 100644 --- a/zed/src/chat_panel.rs +++ b/zed/src/chat_panel.rs @@ -38,7 +38,7 @@ impl ChatPanel { let mut this = Self { channel_list, active_channel: None, - messages: ListState::new(Vec::new()), + messages: ListState::new(Vec::new(), Orientation::Bottom), input_editor, settings, }; @@ -82,6 +82,7 @@ impl ChatPanel { .cursor::<(), ()>() .map(|m| self.render_message(m)) .collect(), + Orientation::Bottom, ); self.active_channel = Some((channel, subscription)); }