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)); }