Re-render all list elements when refreshing windows

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

gpui/src/app.rs           |  43 +++++++--
gpui/src/elements.rs      |  69 +++++++++++---
gpui/src/elements/list.rs | 185 ++++++++++++++++++++++++++++------------
gpui/src/lib.rs           |   2 
zed/src/channel.rs        |  14 +-
zed/src/chat_panel.rs     |  50 +++++-----
6 files changed, 248 insertions(+), 115 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -1305,7 +1305,7 @@ impl MutableAppContext {
         if !self.flushing_effects && self.pending_flushes == 0 {
             self.flushing_effects = true;
 
-            let mut full_refresh = false;
+            let mut refreshing = false;
             loop {
                 if let Some(effect) = self.pending_effects.pop_front() {
                     match effect {
@@ -1320,13 +1320,13 @@ impl MutableAppContext {
                             self.focus(window_id, view_id);
                         }
                         Effect::RefreshWindows => {
-                            full_refresh = true;
+                            refreshing = true;
                         }
                     }
                     self.remove_dropped_entities();
                 } else {
                     self.remove_dropped_entities();
-                    if full_refresh {
+                    if refreshing {
                         self.perform_window_refresh();
                     } else {
                         self.update_windows();
@@ -1336,7 +1336,7 @@ impl MutableAppContext {
                         self.flushing_effects = false;
                         break;
                     } else {
-                        full_refresh = false;
+                        refreshing = false;
                     }
                 }
             }
@@ -1638,11 +1638,11 @@ impl AppContext {
         window_id: usize,
         view_id: usize,
         titlebar_height: f32,
-        refresh: bool,
+        refreshing: bool,
     ) -> Result<ElementBox> {
         self.views
             .get(&(window_id, view_id))
-            .map(|v| v.render(window_id, view_id, titlebar_height, refresh, self))
+            .map(|v| v.render(window_id, view_id, titlebar_height, refreshing, self))
             .ok_or(anyhow!("view not found"))
     }
 
@@ -1666,6 +1666,23 @@ impl AppContext {
             .collect::<HashMap<_, ElementBox>>()
     }
 
+    pub fn render_cx<V: View>(
+        &self,
+        window_id: usize,
+        view_id: usize,
+        titlebar_height: f32,
+        refreshing: bool,
+    ) -> RenderContext<V> {
+        RenderContext {
+            app: self,
+            titlebar_height,
+            refreshing,
+            window_id,
+            view_id,
+            view_type: PhantomData,
+        }
+    }
+
     pub fn background(&self) -> &Arc<executor::Background> {
         &self.background
     }
@@ -1816,7 +1833,7 @@ pub trait AnyView {
         window_id: usize,
         view_id: usize,
         titlebar_height: f32,
-        refresh: bool,
+        refreshing: bool,
         cx: &AppContext,
     ) -> ElementBox;
     fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
@@ -1849,7 +1866,7 @@ where
         window_id: usize,
         view_id: usize,
         titlebar_height: f32,
-        refresh: bool,
+        refreshing: bool,
         cx: &AppContext,
     ) -> ElementBox {
         View::render(
@@ -1860,7 +1877,7 @@ where
                 app: cx,
                 view_type: PhantomData::<T>,
                 titlebar_height,
-                refresh,
+                refreshing,
             },
         )
     }
@@ -2229,7 +2246,7 @@ impl<'a, T: View> ViewContext<'a, T> {
 pub struct RenderContext<'a, T: View> {
     pub app: &'a AppContext,
     pub titlebar_height: f32,
-    pub refresh: bool,
+    pub refreshing: bool,
     window_id: usize,
     view_id: usize,
     view_type: PhantomData<T>,
@@ -2255,6 +2272,12 @@ impl<V: View> Deref for RenderContext<'_, V> {
     }
 }
 
+impl<V: View> ReadModel for RenderContext<'_, V> {
+    fn read_model<T: Entity>(&self, handle: &ModelHandle<T>) -> &T {
+        self.app.read_model(handle)
+    }
+}
+
 impl<M> AsRef<AppContext> for ViewContext<'_, M> {
     fn as_ref(&self) -> &AppContext {
         &self.app.cx

gpui/src/elements.rs 🔗

@@ -37,7 +37,14 @@ use crate::{
 };
 use core::panic;
 use json::ToJson;
-use std::{any::Any, borrow::Cow, mem};
+use std::{
+    any::Any,
+    borrow::Cow,
+    cell::RefCell,
+    mem,
+    ops::{Deref, DerefMut},
+    rc::Rc,
+};
 
 trait AnyElement {
     fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
@@ -91,20 +98,20 @@ pub trait Element {
     where
         Self: 'static + Sized,
     {
-        ElementBox {
+        ElementBox(ElementRc {
             name: None,
-            element: Box::new(Lifecycle::Init { element: self }),
-        }
+            element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
+        })
     }
 
     fn named(self, name: impl Into<Cow<'static, str>>) -> ElementBox
     where
         Self: 'static + Sized,
     {
-        ElementBox {
+        ElementBox(ElementRc {
             name: Some(name.into()),
-            element: Box::new(Lifecycle::Init { element: self }),
-        }
+            element: Rc::new(RefCell::new(Lifecycle::Init { element: self })),
+        })
     }
 }
 
@@ -127,9 +134,12 @@ pub enum Lifecycle<T: Element> {
         paint: T::PaintState,
     },
 }
-pub struct ElementBox {
+pub struct ElementBox(ElementRc);
+
+#[derive(Clone)]
+pub struct ElementRc {
     name: Option<Cow<'static, str>>,
-    element: Box<dyn AnyElement>,
+    element: Rc<RefCell<dyn AnyElement>>,
 }
 
 impl<T: Element> AnyElement for Lifecycle<T> {
@@ -262,28 +272,51 @@ impl<T: Element> Default for Lifecycle<T> {
 }
 
 impl ElementBox {
+    pub fn metadata(&self) -> Option<&dyn Any> {
+        let element = unsafe { &*self.0.element.as_ptr() };
+        element.metadata()
+    }
+}
+
+impl Into<ElementRc> for ElementBox {
+    fn into(self) -> ElementRc {
+        self.0
+    }
+}
+
+impl Deref for ElementBox {
+    type Target = ElementRc;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for ElementBox {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl ElementRc {
     pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F {
-        self.element.layout(constraint, cx)
+        self.element.borrow_mut().layout(constraint, cx)
     }
 
     pub fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) {
-        self.element.paint(origin, cx);
+        self.element.borrow_mut().paint(origin, cx);
     }
 
     pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
-        self.element.dispatch_event(event, cx)
+        self.element.borrow_mut().dispatch_event(event, cx)
     }
 
     pub fn size(&self) -> Vector2F {
-        self.element.size()
-    }
-
-    pub fn metadata(&self) -> Option<&dyn Any> {
-        self.element.metadata()
+        self.element.borrow().size()
     }
 
     pub fn debug(&self, cx: &DebugContext) -> json::Value {
-        let mut value = self.element.debug(cx);
+        let mut value = self.element.borrow().debug(cx);
 
         if let Some(name) = &self.name {
             if let json::Value::Object(map) = &mut value {

gpui/src/elements/list.rs 🔗

@@ -5,19 +5,17 @@ use crate::{
     },
     json::json,
     sum_tree::{self, Bias, SumTree},
-    DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
+    RenderContext, SizeConstraint, View,
 };
-use parking_lot::Mutex;
-use std::{ops::Range, sync::Arc};
-
-use crate::ElementBox;
+use std::{cell::RefCell, ops::Range, rc::Rc};
 
 pub struct List {
     state: ListState,
 }
 
 #[derive(Clone)]
-pub struct ListState(Arc<Mutex<StateInner>>);
+pub struct ListState(Rc<RefCell<StateInner>>);
 
 #[derive(Eq, PartialEq)]
 pub enum Orientation {
@@ -27,7 +25,7 @@ pub enum Orientation {
 
 struct StateInner {
     last_layout_width: f32,
-    elements: Vec<ElementBox>,
+    elements: Vec<Option<ElementRc>>,
     heights: SumTree<ElementHeight>,
     scroll_position: f32,
     orientation: Orientation,
@@ -56,13 +54,55 @@ struct PendingCount(usize);
 struct Height(f32);
 
 impl List {
-    pub fn new(state: ListState) -> Self {
+    pub fn new<F, I, V>(state: ListState, cx: &RenderContext<V>, build_items: F) -> Self
+    where
+        F: Fn(Range<usize>) -> I,
+        I: IntoIterator<Item = ElementBox>,
+        V: View,
+    {
+        {
+            let state = &mut *state.0.borrow_mut();
+            if cx.refreshing {
+                let elements = (build_items)(0..state.elements.len());
+                state.elements.clear();
+                state
+                    .elements
+                    .extend(elements.into_iter().map(|e| Some(e.into())));
+                state.heights = SumTree::new();
+                state.heights.extend(
+                    (0..state.elements.len()).map(|_| ElementHeight::Pending),
+                    &(),
+                );
+            } else {
+                let mut cursor = state.heights.cursor::<PendingCount, Count>();
+                cursor.seek(&PendingCount(1), sum_tree::Bias::Left, &());
+
+                while cursor.item().is_some() {
+                    let start_ix = cursor.sum_start().0;
+                    while cursor.item().map_or(false, |h| h.is_pending()) {
+                        cursor.next(&());
+                    }
+                    let end_ix = cursor.sum_start().0;
+                    if end_ix > start_ix {
+                        state.elements.splice(
+                            start_ix..end_ix,
+                            (build_items)(start_ix..end_ix)
+                                .into_iter()
+                                .map(|e| Some(e.into())),
+                        );
+                    }
+
+                    cursor.seek(&PendingCount(cursor.seek_start().0 + 1), Bias::Left, &());
+                }
+            }
+        }
+
         Self { state }
     }
 }
 
 impl Element for List {
-    type LayoutState = ();
+    type LayoutState = Vec<ElementRc>;
 
     type PaintState = ();
 
@@ -71,7 +111,7 @@ impl Element for List {
         constraint: SizeConstraint,
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
-        let state = &mut *self.state.0.lock();
+        let state = &mut *self.state.0.borrow_mut();
         let mut item_constraint = constraint;
         item_constraint.min.set_y(0.);
         item_constraint.max.set_y(f32::INFINITY);
@@ -87,8 +127,8 @@ impl Element for List {
 
             while let Some(height) = old_heights.item() {
                 if height.is_pending() {
-                    let size =
-                        state.elements[old_heights.sum_start().count].layout(item_constraint, cx);
+                    let element = &mut state.elements[old_heights.sum_start().count];
+                    let size = element.as_mut().unwrap().layout(item_constraint, cx);
                     new_heights.push(ElementHeight::Ready(size.y()), &());
 
                     // Adjust scroll position to keep visible elements stable
@@ -123,18 +163,28 @@ impl Element for List {
         } else {
             state.heights = SumTree::new();
             for element in &mut state.elements {
+                let element = element.as_mut().unwrap();
                 let size = element.layout(item_constraint, cx);
                 state.heights.push(ElementHeight::Ready(size.y()), &());
             }
             state.last_layout_width = constraint.max.x();
         }
 
-        (size, ())
+        let visible_elements = state.elements[state.visible_range(size.y())]
+            .iter()
+            .map(|e| e.clone().unwrap())
+            .collect();
+        (size, visible_elements)
     }
 
-    fn paint(&mut self, bounds: RectF, _: &mut (), cx: &mut PaintContext) {
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_elements: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) {
         cx.scene.push_layer(Some(bounds));
-        let state = &mut *self.state.0.lock();
+        let state = &mut *self.state.0.borrow_mut();
         let visible_range = state.visible_range(bounds.height());
 
         let mut item_top = {
@@ -149,7 +199,7 @@ impl Element for List {
         }
         let scroll_top = state.scroll_top(bounds.height());
 
-        for element in &mut state.elements[visible_range] {
+        for element in visible_elements {
             let origin = bounds.origin() + vec2f(0., item_top - scroll_top);
             element.paint(origin, cx);
             item_top += element.size().y();
@@ -161,16 +211,15 @@ impl Element for List {
         &mut self,
         event: &Event,
         bounds: RectF,
-        _: &mut (),
+        visible_elements: &mut Self::LayoutState,
         _: &mut (),
         cx: &mut EventContext,
     ) -> bool {
         let mut handled = false;
 
-        let mut state = self.state.0.lock();
-        let visible_range = state.visible_range(bounds.height());
-        for item in &mut state.elements[visible_range] {
-            handled = item.dispatch_event(event, cx) || handled;
+        let mut state = self.state.0.borrow_mut();
+        for element in visible_elements {
+            handled = element.dispatch_event(event, cx) || handled;
         }
 
         match event {
@@ -191,10 +240,16 @@ impl Element for List {
         handled
     }
 
-    fn debug(&self, bounds: RectF, _: &(), _: &(), cx: &DebugContext) -> serde_json::Value {
-        let state = self.state.0.lock();
+    fn debug(
+        &self,
+        bounds: RectF,
+        visible_elements: &Self::LayoutState,
+        _: &(),
+        cx: &DebugContext,
+    ) -> serde_json::Value {
+        let state = self.state.0.borrow_mut();
         let visible_range = state.visible_range(bounds.height());
-        let visible_elements = state.elements[visible_range.clone()]
+        let visible_elements = visible_elements
             .iter()
             .map(|e| e.debug(cx))
             .collect::<Vec<_>>();
@@ -207,40 +262,29 @@ impl Element for List {
 }
 
 impl ListState {
-    pub fn new(elements: Vec<ElementBox>, orientation: Orientation) -> Self {
+    pub fn new(element_count: usize, orientation: Orientation) -> Self {
         let mut heights = SumTree::new();
-        heights.extend(elements.iter().map(|_| ElementHeight::Pending), &());
-        Self(Arc::new(Mutex::new(StateInner {
+        heights.extend((0..element_count).map(|_| ElementHeight::Pending), &());
+        Self(Rc::new(RefCell::new(StateInner {
             last_layout_width: 0.,
-            elements,
+            elements: (0..element_count).map(|_| None).collect(),
             heights,
             scroll_position: 0.,
             orientation,
         })))
     }
 
-    pub fn splice(
-        &self,
-        old_range: Range<usize>,
-        new_elements: impl IntoIterator<Item = ElementBox>,
-    ) {
-        let state = &mut *self.0.lock();
+    pub fn splice(&self, old_range: Range<usize>, count: usize) {
+        let state = &mut *self.0.borrow_mut();
 
         let mut old_heights = state.heights.cursor::<Count, ()>();
         let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
         old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
 
-        let mut len = 0;
-        let old_elements = state.elements.splice(
-            old_range,
-            new_elements.into_iter().map(|e| {
-                len += 1;
-                e
-            }),
-        );
+        let old_elements = state.elements.splice(old_range, (0..count).map(|_| None));
         drop(old_elements);
 
-        new_heights.extend((0..len).map(|_| ElementHeight::Pending), &());
+        new_heights.extend((0..count).map(|_| ElementHeight::Pending), &());
         new_heights.push_tree(old_heights.suffix(&()), &());
         drop(old_heights);
         state.heights = new_heights;
@@ -370,22 +414,28 @@ impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::*, geometry::vector::vec2f};
+    use crate::{elements::*, geometry::vector::vec2f, Entity};
 
     #[crate::test(self)]
     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.)], Orientation::Top);
-        let mut list = List::new(state.clone()).boxed();
+        let mut presenter = cx.build_presenter(0, 0.);
+
+        let mut elements = vec![20., 30., 10.];
+        let state = ListState::new(elements.len(), Orientation::Top);
 
+        let mut list = List::new(
+            state.clone(),
+            &cx.render_cx::<TestView>(0, 0, 0., false),
+            |range| elements[range].iter().copied().map(item),
+        )
+        .boxed();
         let size = list.layout(
             SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
-            &mut layout_cx,
+            &mut presenter.layout_cx(cx),
         );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
-            state.0.lock().heights.summary(),
+            state.0.borrow().heights.summary(),
             ElementHeightSummary {
                 count: 3,
                 pending_count: 0,
@@ -393,23 +443,32 @@ mod tests {
             }
         );
 
-        state.splice(1..2, vec![item(40.), item(50.)]);
-        state.splice(3..3, vec![item(60.)]);
+        elements.splice(1..2, vec![40., 50.]);
+        elements.push(60.);
+        state.splice(1..2, 2);
+        state.splice(4..4, 1);
         assert_eq!(
-            state.0.lock().heights.summary(),
+            state.0.borrow().heights.summary(),
             ElementHeightSummary {
                 count: 5,
                 pending_count: 3,
                 height: 30.
             }
         );
+
+        let mut list = List::new(
+            state.clone(),
+            &cx.render_cx::<TestView>(0, 0, 0., false),
+            |range| elements[range].iter().copied().map(item),
+        )
+        .boxed();
         let size = list.layout(
             SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
-            &mut layout_cx,
+            &mut presenter.layout_cx(cx),
         );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
-            state.0.lock().heights.summary(),
+            state.0.borrow().heights.summary(),
             ElementHeightSummary {
                 count: 5,
                 pending_count: 0,
@@ -424,4 +483,20 @@ mod tests {
             .with_width(100.)
             .boxed()
     }
+
+    struct TestView;
+
+    impl Entity for TestView {
+        type Event = ();
+    }
+
+    impl View for TestView {
+        fn ui_name() -> &'static str {
+            "TestView"
+        }
+
+        fn render(&self, _: &RenderContext<'_, Self>) -> ElementBox {
+            unimplemented!()
+        }
+    }
 }

gpui/src/lib.rs 🔗

@@ -18,7 +18,7 @@ pub use scene::{Border, Quad, Scene};
 pub mod text_layout;
 pub use text_layout::TextLayoutCache;
 mod util;
-pub use elements::{Element, ElementBox};
+pub use elements::{Element, ElementBox, ElementRc};
 pub mod executor;
 pub use executor::Task;
 pub mod color;

zed/src/channel.rs 🔗

@@ -60,7 +60,7 @@ pub struct PendingChannelMessage {
 #[derive(Clone, Debug, Default)]
 pub struct ChannelMessageSummary {
     max_id: u64,
-    count: Count,
+    count: usize,
 }
 
 #[derive(Copy, Clone, Debug, Default)]
@@ -208,7 +208,7 @@ impl Channel {
                     }
 
                     channel.update(&mut cx, |channel, cx| {
-                        let old_count = channel.messages.summary().count.0;
+                        let old_count = channel.messages.summary().count;
                         let new_count = messages.len();
 
                         channel.messages = SumTree::new();
@@ -282,6 +282,10 @@ impl Channel {
         Ok(())
     }
 
+    pub fn message_count(&self) -> usize {
+        self.messages.summary().count
+    }
+
     pub fn messages(&self) -> &SumTree<ChannelMessage> {
         &self.messages
     }
@@ -380,7 +384,7 @@ impl sum_tree::Item for ChannelMessage {
     fn summary(&self) -> Self::Summary {
         ChannelMessageSummary {
             max_id: self.id,
-            count: Count(1),
+            count: 1,
         }
     }
 }
@@ -390,7 +394,7 @@ impl sum_tree::Summary for ChannelMessageSummary {
 
     fn add_summary(&mut self, summary: &Self, _: &()) {
         self.max_id = summary.max_id;
-        self.count.0 += summary.count.0;
+        self.count += summary.count;
     }
 }
 
@@ -403,7 +407,7 @@ impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for u64 {
 
 impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
     fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
-        self.0 += summary.count.0;
+        self.0 += summary.count;
     }
 }
 

zed/src/chat_panel.rs 🔗

@@ -14,7 +14,7 @@ use time::{OffsetDateTime, UtcOffset};
 pub struct ChatPanel {
     channel_list: ModelHandle<ChannelList>,
     active_channel: Option<(ModelHandle<Channel>, Subscription)>,
-    messages: ListState,
+    message_list: ListState,
     input_editor: ViewHandle<Editor>,
     settings: watch::Receiver<Settings>,
 }
@@ -38,8 +38,8 @@ impl ChatPanel {
         let input_editor = cx.add_view(|cx| Editor::auto_height(settings.clone(), cx));
         let mut this = Self {
             channel_list,
-            active_channel: None,
-            messages: ListState::new(Vec::new(), Orientation::Bottom),
+            active_channel: Default::default(),
+            message_list: ListState::new(0, Orientation::Bottom),
             input_editor,
             settings,
         };
@@ -76,23 +76,15 @@ impl ChatPanel {
     fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
         if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
             let subscription = cx.subscribe(&channel, Self::channel_did_change);
-            let now = OffsetDateTime::now_utc();
-            self.messages = ListState::new(
-                channel
-                    .read(cx)
-                    .messages()
-                    .cursor::<(), ()>()
-                    .map(|m| self.render_message(m, now))
-                    .collect(),
-                Orientation::Bottom,
-            );
+            self.message_list =
+                ListState::new(channel.read(cx).message_count(), Orientation::Bottom);
             self.active_channel = Some((channel, subscription));
         }
     }
 
     fn channel_did_change(
         &mut self,
-        channel: ModelHandle<Channel>,
+        _: ModelHandle<Channel>,
         event: &ChannelEvent,
         cx: &mut ViewContext<Self>,
     ) {
@@ -101,21 +93,27 @@ impl ChatPanel {
                 old_range,
                 new_count,
             } => {
-                let now = OffsetDateTime::now_utc();
-                self.messages.splice(
-                    old_range.clone(),
-                    channel
-                        .read(cx)
-                        .messages_in_range(old_range.start..(old_range.start + new_count))
-                        .map(|message| self.render_message(message, now)),
-                );
+                self.message_list.splice(old_range.clone(), *new_count);
             }
         }
         cx.notify();
     }
 
-    fn render_active_channel_messages(&self) -> ElementBox {
-        Expanded::new(1., List::new(self.messages.clone()).boxed()).boxed()
+    fn render_active_channel_messages(&self, cx: &RenderContext<Self>) -> ElementBox {
+        let messages = if let Some((channel, _)) = self.active_channel.as_ref() {
+            let channel = channel.read(cx);
+            let now = OffsetDateTime::now_utc();
+            List::new(self.message_list.clone(), cx, |range| {
+                channel
+                    .messages_in_range(range)
+                    .map(|message| self.render_message(message, now))
+            })
+            .boxed()
+        } else {
+            Empty::new().boxed()
+        };
+
+        Expanded::new(1., messages).boxed()
     }
 
     fn render_message(&self, message: &ChannelMessage, now: OffsetDateTime) -> ElementBox {
@@ -194,11 +192,11 @@ impl View for ChatPanel {
         "ChatPanel"
     }
 
-    fn render(&self, _: &RenderContext<Self>) -> ElementBox {
+    fn render(&self, cx: &RenderContext<Self>) -> ElementBox {
         let theme = &self.settings.borrow().theme;
         Container::new(
             Flex::column()
-                .with_child(self.render_active_channel_messages())
+                .with_child(self.render_active_channel_messages(cx))
                 .with_child(self.render_input_box())
                 .boxed(),
         )