Pass a RenderContext to UniformList

Nathan Sobo and Max Brunsfeld created

In some cases, we need to render during layout. Previously, we were rendering with a LayoutContext in some cases, but this commit adds the ability to retrieve a render context with a given handle and we use that feature in UniformList.

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/editor/src/editor.rs               | 131 +++++++++++++-----------
crates/editor/src/element.rs              |   6 
crates/gpui/src/app.rs                    |  61 ++++++----
crates/gpui/src/elements/uniform_list.rs  |  42 ++++---
crates/gpui/src/presenter.rs              |  35 +++++
crates/gpui/src/views/select.rs           |   8 
crates/picker/src/picker.rs               |   8 
crates/project_panel/src/project_panel.rs |  39 +++----
8 files changed, 194 insertions(+), 136 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -592,11 +592,11 @@ impl ContextMenu {
         &self,
         cursor_position: DisplayPoint,
         style: EditorStyle,
-        cx: &AppContext,
+        cx: &mut RenderContext<Editor>,
     ) -> (DisplayPoint, ElementBox) {
         match self {
             ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
-            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
         }
     }
 }
@@ -633,54 +633,62 @@ impl CompletionsMenu {
         !self.matches.is_empty()
     }
 
-    fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
+    fn render(&self, style: EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
         enum CompletionTag {}
 
         let completions = self.completions.clone();
         let matches = self.matches.clone();
         let selected_item = self.selected_item;
         let container_style = style.autocomplete.container;
-        UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
-            let start_ix = range.start;
-            for (ix, mat) in matches[range].iter().enumerate() {
-                let completion = &completions[mat.candidate_id];
-                let item_ix = start_ix + ix;
-                items.push(
-                    MouseEventHandler::new::<CompletionTag, _, _>(
-                        mat.candidate_id,
-                        cx,
-                        |state, _| {
-                            let item_style = if item_ix == selected_item {
-                                style.autocomplete.selected_item
-                            } else if state.hovered {
-                                style.autocomplete.hovered_item
-                            } else {
-                                style.autocomplete.item
-                            };
-
-                            Text::new(completion.label.text.clone(), style.text.clone())
-                                .with_soft_wrap(false)
-                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
-                                    &completion.label.text,
-                                    style.text.color.into(),
-                                    styled_runs_for_code_label(&completion.label, &style.syntax),
-                                    &mat.positions,
-                                ))
-                                .contained()
-                                .with_style(item_style)
-                                .boxed()
-                        },
-                    )
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_mouse_down(move |cx| {
-                        cx.dispatch_action(ConfirmCompletion {
-                            item_ix: Some(item_ix),
-                        });
-                    })
-                    .boxed(),
-                );
-            }
-        })
+        UniformList::new(
+            self.list.clone(),
+            matches.len(),
+            cx,
+            move |_, range, items, cx| {
+                let start_ix = range.start;
+                for (ix, mat) in matches[range].iter().enumerate() {
+                    let completion = &completions[mat.candidate_id];
+                    let item_ix = start_ix + ix;
+                    items.push(
+                        MouseEventHandler::new::<CompletionTag, _, _>(
+                            mat.candidate_id,
+                            cx,
+                            |state, _| {
+                                let item_style = if item_ix == selected_item {
+                                    style.autocomplete.selected_item
+                                } else if state.hovered {
+                                    style.autocomplete.hovered_item
+                                } else {
+                                    style.autocomplete.item
+                                };
+
+                                Text::new(completion.label.text.clone(), style.text.clone())
+                                    .with_soft_wrap(false)
+                                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                                        &completion.label.text,
+                                        style.text.color.into(),
+                                        styled_runs_for_code_label(
+                                            &completion.label,
+                                            &style.syntax,
+                                        ),
+                                        &mat.positions,
+                                    ))
+                                    .contained()
+                                    .with_style(item_style)
+                                    .boxed()
+                            },
+                        )
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_mouse_down(move |cx| {
+                            cx.dispatch_action(ConfirmCompletion {
+                                item_ix: Some(item_ix),
+                            });
+                        })
+                        .boxed(),
+                    );
+                }
+            },
+        )
         .with_width_from_item(
             self.matches
                 .iter()
@@ -772,14 +780,18 @@ impl CodeActionsMenu {
         &self,
         mut cursor_position: DisplayPoint,
         style: EditorStyle,
+        cx: &mut RenderContext<Editor>,
     ) -> (DisplayPoint, ElementBox) {
         enum ActionTag {}
 
         let container_style = style.autocomplete.container;
         let actions = self.actions.clone();
         let selected_item = self.selected_item;
-        let element =
-            UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
+        let element = UniformList::new(
+            self.list.clone(),
+            actions.len(),
+            cx,
+            move |_, range, items, cx| {
                 let start_ix = range.start;
                 for (ix, action) in actions[range].iter().enumerate() {
                     let item_ix = start_ix + ix;
@@ -808,17 +820,18 @@ impl CodeActionsMenu {
                         .boxed(),
                     );
                 }
-            })
-            .with_width_from_item(
-                self.actions
-                    .iter()
-                    .enumerate()
-                    .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
-                    .map(|(ix, _)| ix),
-            )
-            .contained()
-            .with_style(container_style)
-            .boxed();
+            },
+        )
+        .with_width_from_item(
+            self.actions
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+                .map(|(ix, _)| ix),
+        )
+        .contained()
+        .with_style(container_style)
+        .boxed();
 
         if self.deployed_from_indicator {
             *cursor_position.column_mut() = 0;
@@ -2578,7 +2591,7 @@ impl Editor {
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
-        cx: &mut ViewContext<Self>,
+        cx: &mut RenderContext<Self>,
     ) -> Option<ElementBox> {
         if self.available_code_actions.is_some() {
             enum Tag {}
@@ -2612,7 +2625,7 @@ impl Editor {
         &self,
         cursor_position: DisplayPoint,
         style: EditorStyle,
-        cx: &AppContext,
+        cx: &mut RenderContext<Editor>,
     ) -> Option<(DisplayPoint, ElementBox)> {
         self.context_menu
             .as_ref()

crates/editor/src/element.rs 🔗

@@ -1024,8 +1024,6 @@ impl Element for EditorElement {
             max_row.saturating_sub(1) as f32,
         );
 
-        let mut context_menu = None;
-        let mut code_actions_indicator = None;
         self.update_view(cx.app, |view, cx| {
             let clamped = view.clamp_scroll_left(scroll_max.x());
             let autoscrolled;
@@ -1045,7 +1043,11 @@ impl Element for EditorElement {
             if clamped || autoscrolled {
                 snapshot = view.snapshot(cx);
             }
+        });
 
+        let mut context_menu = None;
+        let mut code_actions_indicator = None;
+        cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
             let newest_selection_head = view
                 .selections
                 .newest::<usize>(cx)

crates/gpui/src/app.rs 🔗

@@ -468,6 +468,26 @@ impl TestAppContext {
         result
     }
 
+    pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
+    where
+        F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
+        V: View,
+    {
+        handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
+            let mut render_cx = RenderContext {
+                app: cx,
+                window_id: handle.window_id(),
+                view_id: handle.id(),
+                view_type: PhantomData,
+                titlebar_height: 0.,
+                hovered_region_id: None,
+                clicked_region_id: None,
+                refreshing: false,
+            };
+            f(view, &mut render_cx)
+        })
+    }
+
     pub fn to_async(&self) -> AsyncAppContext {
         AsyncAppContext(self.cx.clone())
     }
@@ -1756,27 +1776,6 @@ impl MutableAppContext {
         )
     }
 
-    pub fn build_render_context<V: View>(
-        &mut self,
-        window_id: usize,
-        view_id: usize,
-        titlebar_height: f32,
-        hovered_region_id: Option<MouseRegionId>,
-        clicked_region_id: Option<MouseRegionId>,
-        refreshing: bool,
-    ) -> RenderContext<V> {
-        RenderContext {
-            app: self,
-            window_id,
-            view_id,
-            view_type: PhantomData,
-            titlebar_height,
-            hovered_region_id,
-            clicked_region_id,
-            refreshing,
-        }
-    }
-
     pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
     where
         T: View,
@@ -3429,13 +3428,13 @@ pub struct RenderParams {
 }
 
 pub struct RenderContext<'a, T: View> {
+    pub(crate) window_id: usize,
+    pub(crate) view_id: usize,
+    pub(crate) view_type: PhantomData<T>,
+    pub(crate) hovered_region_id: Option<MouseRegionId>,
+    pub(crate) clicked_region_id: Option<MouseRegionId>,
     pub app: &'a mut MutableAppContext,
-    window_id: usize,
-    view_id: usize,
-    view_type: PhantomData<T>,
     pub titlebar_height: f32,
-    hovered_region_id: Option<MouseRegionId>,
-    clicked_region_id: Option<MouseRegionId>,
     pub refreshing: bool,
 }
 
@@ -3587,6 +3586,16 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
     }
 }
 
+impl<V: View> UpgradeViewHandle for RenderContext<'_, V> {
+    fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
+        self.cx.upgrade_view_handle(handle)
+    }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.cx.upgrade_any_view_handle(handle)
+    }
+}
+
 impl<V: View> UpdateModel for ViewContext<'_, V> {
     fn update_model<T: Entity, O>(
         &mut self,

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{self, json},
-    ElementBox,
+    ElementBox, RenderContext, View,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -41,27 +41,40 @@ pub struct LayoutState {
     items: Vec<ElementBox>,
 }
 
-pub struct UniformList<F>
-where
-    F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
-{
+pub struct UniformList {
     state: UniformListState,
     item_count: usize,
-    append_items: F,
+    append_items: Box<dyn Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext) -> bool>,
     padding_top: f32,
     padding_bottom: f32,
     get_width_from_item: Option<usize>,
 }
 
-impl<F> UniformList<F>
-where
-    F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
-{
-    pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
+impl UniformList {
+    pub fn new<F, V>(
+        state: UniformListState,
+        item_count: usize,
+        cx: &mut RenderContext<V>,
+        append_items: F,
+    ) -> Self
+    where
+        V: View,
+        F: 'static + Fn(&mut V, Range<usize>, &mut Vec<ElementBox>, &mut RenderContext<V>),
+    {
+        let handle = cx.handle();
         Self {
             state,
             item_count,
-            append_items,
+            append_items: Box::new(move |range, items, cx| {
+                if let Some(handle) = handle.upgrade(cx) {
+                    cx.render(&handle, |view, cx| {
+                        append_items(view, range, items, cx);
+                    });
+                    true
+                } else {
+                    false
+                }
+            }),
             padding_top: 0.,
             padding_bottom: 0.,
             get_width_from_item: None,
@@ -144,10 +157,7 @@ where
     }
 }
 
-impl<F> Element for UniformList<F>
-where
-    F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
-{
+impl Element for UniformList {
     type LayoutState = LayoutState;
     type PaintState = ();
 

crates/gpui/src/presenter.rs 🔗

@@ -9,13 +9,14 @@ use crate::{
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
     ElementStateContext, Entity, FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel,
-    ReadView, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
-    WeakModelHandle, WeakViewHandle,
+    ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
 use std::{
     collections::{HashMap, HashSet},
+    marker::PhantomData,
     ops::{Deref, DerefMut},
     sync::Arc,
 };
@@ -172,12 +173,15 @@ impl Presenter {
         LayoutContext {
             rendered_views: &mut self.rendered_views,
             parents: &mut self.parents,
-            refreshing,
             font_cache: &self.font_cache,
             font_system: cx.platform().fonts(),
             text_layout_cache: &self.text_layout_cache,
             asset_cache: &self.asset_cache,
             view_stack: Vec::new(),
+            refreshing,
+            hovered_region_id: self.hovered_region_id,
+            clicked_region_id: self.clicked_region.as_ref().map(MouseRegion::id),
+            titlebar_height: self.titlebar_height,
             app: cx,
         }
     }
@@ -342,12 +346,15 @@ pub struct LayoutContext<'a> {
     rendered_views: &'a mut HashMap<usize, ElementBox>,
     parents: &'a mut HashMap<usize, usize>,
     view_stack: Vec<usize>,
-    pub refreshing: bool,
     pub font_cache: &'a Arc<FontCache>,
     pub font_system: Arc<dyn FontSystem>,
     pub text_layout_cache: &'a TextLayoutCache,
     pub asset_cache: &'a AssetCache,
     pub app: &'a mut MutableAppContext,
+    pub refreshing: bool,
+    titlebar_height: f32,
+    hovered_region_id: Option<MouseRegionId>,
+    clicked_region_id: Option<MouseRegionId>,
 }
 
 impl<'a> LayoutContext<'a> {
@@ -362,6 +369,26 @@ impl<'a> LayoutContext<'a> {
         self.view_stack.pop();
         size
     }
+
+    pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
+    where
+        F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
+        V: View,
+    {
+        handle.update(self.app, |view, cx| {
+            let mut render_cx = RenderContext {
+                app: cx,
+                window_id: handle.window_id(),
+                view_id: handle.id(),
+                view_type: PhantomData,
+                titlebar_height: self.titlebar_height,
+                hovered_region_id: self.hovered_region_id,
+                clicked_region_id: self.clicked_region_id,
+                refreshing: self.refreshing,
+            };
+            f(view, &mut render_cx)
+        })
+    }
 }
 
 impl<'a> Deref for LayoutContext<'a> {

crates/gpui/src/views/select.rs 🔗

@@ -123,7 +123,6 @@ impl View for Select {
             .boxed(),
         );
         if self.is_open {
-            let handle = self.handle.clone();
             result.add_child(
                 Overlay::new(
                     Container::new(
@@ -131,9 +130,8 @@ impl View for Select {
                             UniformList::new(
                                 self.list_state.clone(),
                                 self.item_count,
-                                move |mut range, items, cx| {
-                                    let handle = handle.upgrade(cx).unwrap();
-                                    let this = handle.read(cx);
+                                cx,
+                                move |this, mut range, items, cx| {
                                     let selected_item_ix = this.selected_item_ix;
                                     range.end = range.end.min(this.item_count);
                                     items.extend(range.map(|ix| {
@@ -141,7 +139,7 @@ impl View for Select {
                                             ix,
                                             cx,
                                             |mouse_state, cx| {
-                                                (handle.read(cx).render_item)(
+                                                (this.render_item)(
                                                     ix,
                                                     if ix == selected_item_ix {
                                                         ItemType::Selected

crates/picker/src/picker.rs 🔗

@@ -54,6 +54,7 @@ impl<D: PickerDelegate> View for Picker<D> {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
         let settings = cx.global::<Settings>();
+        let container_style = settings.theme.picker.container;
         let delegate = self.delegate.clone();
         let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
             delegate.read(cx).match_count()
@@ -80,8 +81,9 @@ impl<D: PickerDelegate> View for Picker<D> {
                     UniformList::new(
                         self.list_state.clone(),
                         match_count,
-                        move |mut range, items, cx| {
-                            let delegate = delegate.upgrade(cx).unwrap();
+                        cx,
+                        move |this, mut range, items, cx| {
+                            let delegate = this.delegate.upgrade(cx).unwrap();
                             let selected_ix = delegate.read(cx).selected_index();
                             range.end = cmp::min(range.end, delegate.read(cx).match_count());
                             items.extend(range.map(move |ix| {
@@ -103,7 +105,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                 .boxed(),
             )
             .contained()
-            .with_style(settings.theme.picker.container)
+            .with_style(container_style)
             .constrained()
             .with_max_width(self.max_size.x())
             .with_max_height(self.max_size.y())

crates/project_panel/src/project_panel.rs 🔗

@@ -9,8 +9,8 @@ use gpui::{
     },
     impl_internal_actions, keymap,
     platform::CursorStyle,
-    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel,
+    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use settings::Settings;
@@ -706,8 +706,8 @@ impl ProjectPanel {
     fn for_each_visible_entry(
         &self,
         range: Range<usize>,
-        cx: &mut ViewContext<ProjectPanel>,
-        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
+        cx: &mut RenderContext<ProjectPanel>,
+        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut RenderContext<ProjectPanel>),
     ) {
         let mut ix = 0;
         for (worktree_id, visible_worktree_entries) in &self.visible_entries {
@@ -780,7 +780,7 @@ impl ProjectPanel {
         details: EntryDetails,
         editor: &ViewHandle<Editor>,
         theme: &theme::ProjectPanel,
-        cx: &mut ViewContext<Self>,
+        cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         let kind = details.kind;
         let show_editor = details.is_editing && !details.is_processing;
@@ -861,31 +861,28 @@ impl View for ProjectPanel {
         "ProjectPanel"
     }
 
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox {
         let theme = &cx.global::<Settings>().theme.project_panel;
         let mut container_style = theme.container;
         let padding = std::mem::take(&mut container_style.padding);
-        let handle = self.handle.clone();
         UniformList::new(
             self.list.clone(),
             self.visible_entries
                 .iter()
                 .map(|(_, worktree_entries)| worktree_entries.len())
                 .sum(),
-            move |range, items, cx| {
+            cx,
+            move |this, range, items, cx| {
                 let theme = cx.global::<Settings>().theme.clone();
-                let this = handle.upgrade(cx).unwrap();
-                this.update(cx.app, |this, cx| {
-                    this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
-                        items.push(Self::render_entry(
-                            id,
-                            details,
-                            &this.filename_editor,
-                            &theme.project_panel,
-                            cx,
-                        ));
-                    });
-                })
+                this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
+                    items.push(Self::render_entry(
+                        id,
+                        details,
+                        &this.filename_editor,
+                        &theme.project_panel,
+                        cx,
+                    ));
+                });
             },
         )
         .with_padding_top(padding.top)
@@ -1343,7 +1340,7 @@ mod tests {
         let mut result = Vec::new();
         let mut project_entries = HashSet::new();
         let mut has_editor = false;
-        panel.update(cx, |panel, cx| {
+        cx.render(panel, |panel, cx| {
             panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
                 if details.is_editing {
                     assert!(!has_editor, "duplicate editor entry");