gpui: Simplify uniform list API by removing entity param (#32480)

Ben Kunkle and Mikayla created

This PR also introduces `Context::processor`, a sibling of
`Context::listener` that takes a strong pointer to entity and allows for
a return result.

Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/agent/src/thread_history.rs                        |  5 
crates/debugger_ui/src/session/running/breakpoint_list.rs |  6 
crates/debugger_ui/src/session/running/module_list.rs     |  7 +
crates/debugger_ui/src/session/running/variable_list.rs   |  5 
crates/editor/src/code_context_menus.rs                   | 10 +-
crates/extensions_ui/src/extensions_ui.rs                 | 14 +---
crates/git_ui/src/git_panel.rs                            | 11 ++-
crates/gpui/examples/data_table.rs                        | 12 +-
crates/gpui/examples/uniform_list.rs                      |  5 
crates/gpui/src/app/context.rs                            | 12 +++
crates/gpui/src/elements/uniform_list.rs                  | 29 +++-----
crates/language_tools/src/syntax_tree_view.rs             |  5 
crates/outline_panel/src/outline_panel.rs                 | 10 +-
crates/picker/src/picker.rs                               |  7 -
crates/project_panel/src/project_panel.rs                 |  6 
crates/semantic_index/src/project_index_debug_view.rs     |  7 -
crates/zed/src/zed/component_preview.rs                   |  6 
17 files changed, 80 insertions(+), 77 deletions(-)

Detailed changes

crates/agent/src/thread_history.rs 🔗

@@ -594,10 +594,11 @@ impl Render for ThreadHistory {
                     view.pr_5()
                         .child(
                             uniform_list(
-                                cx.entity().clone(),
                                 "thread-history",
                                 self.list_item_count(),
-                                Self::list_items,
+                                cx.processor(|this, range: Range<usize>, window, cx| {
+                                    this.list_items(range, window, cx)
+                                }),
                             )
                             .p_1()
                             .track_scroll(self.scroll_handle.clone())

crates/debugger_ui/src/session/running/breakpoint_list.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    ops::Range,
     path::{Path, PathBuf},
     sync::Arc,
     time::Duration,
@@ -277,10 +278,9 @@ impl BreakpointList {
         let selected_ix = self.selected_ix;
         let focus_handle = self.focus_handle.clone();
         uniform_list(
-            cx.entity(),
             "breakpoint-list",
             self.breakpoints.len(),
-            move |this, range, window, cx| {
+            cx.processor(move |this, range: Range<usize>, window, cx| {
                 range
                     .clone()
                     .zip(&mut this.breakpoints[range])
@@ -291,7 +291,7 @@ impl BreakpointList {
                             .into_any_element()
                     })
                     .collect()
-            },
+            }),
         )
         .track_scroll(self.scroll_handle.clone())
         .flex_grow()

crates/debugger_ui/src/session/running/module_list.rs 🔗

@@ -8,7 +8,7 @@ use project::{
     ProjectItem as _, ProjectPath,
     debugger::session::{Session, SessionEvent},
 };
-use std::{path::Path, sync::Arc};
+use std::{ops::Range, path::Path, sync::Arc};
 use ui::{Scrollbar, ScrollbarState, prelude::*};
 use workspace::Workspace;
 
@@ -281,10 +281,11 @@ impl ModuleList {
 
     fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         uniform_list(
-            cx.entity(),
             "module-list",
             self.entries.len(),
-            |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
+            cx.processor(|this, range: Range<usize>, _window, cx| {
+                range.map(|ix| this.render_entry(ix, cx)).collect()
+            }),
         )
         .track_scroll(self.scroll_handle.clone())
         .size_full()

crates/debugger_ui/src/session/running/variable_list.rs 🔗

@@ -980,10 +980,11 @@ impl Render for VariableList {
             .on_action(cx.listener(Self::edit_variable))
             .child(
                 uniform_list(
-                    cx.entity().clone(),
                     "variable-list",
                     self.entries.len(),
-                    move |this, range, window, cx| this.render_entries(range, window, cx),
+                    cx.processor(move |this, range: Range<usize>, window, cx| {
+                        this.render_entries(range, window, cx)
+                    }),
                 )
                 .track_scroll(self.list_handle.clone())
                 .gap_1_5()

crates/editor/src/code_context_menus.rs 🔗

@@ -722,10 +722,9 @@ impl CompletionsMenu {
         let last_rendered_range = self.last_rendered_range.clone();
         let style = style.clone();
         let list = uniform_list(
-            cx.entity().clone(),
             "completions",
             self.entries.borrow().len(),
-            move |_editor, range, _window, cx| {
+            cx.processor(move |_editor, range: Range<usize>, _window, cx| {
                 last_rendered_range.borrow_mut().replace(range.clone());
                 let start_ix = range.start;
                 let completions_guard = completions.borrow_mut();
@@ -837,7 +836,7 @@ impl CompletionsMenu {
                         )
                     })
                     .collect()
-            },
+            }),
         )
         .occlude()
         .max_h(max_height_in_lines as f32 * window.line_height())
@@ -1452,10 +1451,9 @@ impl CodeActionsMenu {
         let actions = self.actions.clone();
         let selected_item = self.selected_item;
         let list = uniform_list(
-            cx.entity().clone(),
             "code_actions_menu",
             self.actions.len(),
-            move |_this, range, _, cx| {
+            cx.processor(move |_this, range: Range<usize>, _, cx| {
                 actions
                     .iter()
                     .skip(range.start)
@@ -1518,7 +1516,7 @@ impl CodeActionsMenu {
                         )
                     })
                     .collect()
-            },
+            }),
         )
         .occlude()
         .max_h(max_height_in_lines as f32 * window.line_height())

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -1477,18 +1477,12 @@ impl Render for ExtensionsPage {
                             return this.py_4().child(self.render_empty_state(cx));
                         }
 
-                        let extensions_page = cx.entity().clone();
                         let scroll_handle = self.list.clone();
                         this.child(
-                            uniform_list(
-                                extensions_page,
-                                "entries",
-                                count,
-                                Self::render_extensions,
-                            )
-                            .flex_grow()
-                            .pb_4()
-                            .track_scroll(scroll_handle),
+                            uniform_list("entries", count, cx.processor(Self::render_extensions))
+                                .flex_grow()
+                                .pb_4()
+                                .track_scroll(scroll_handle),
                         )
                         .child(
                             div()

crates/git_ui/src/git_panel.rs 🔗

@@ -55,6 +55,7 @@ use project::{
 use serde::{Deserialize, Serialize};
 use settings::{Settings as _, SettingsStore};
 use std::future::Future;
+use std::ops::Range;
 use std::path::{Path, PathBuf};
 use std::{collections::HashSet, sync::Arc, time::Duration, usize};
 use strum::{IntoEnumIterator, VariantNames};
@@ -3710,8 +3711,10 @@ impl GitPanel {
                     .relative()
                     .overflow_hidden()
                     .child(
-                        uniform_list(cx.entity().clone(), "entries", entry_count, {
-                            move |this, range, window, cx| {
+                        uniform_list(
+                            "entries",
+                            entry_count,
+                            cx.processor(move |this, range: Range<usize>, window, cx| {
                                 let mut items = Vec::with_capacity(range.end - range.start);
 
                                 for ix in range {
@@ -3739,8 +3742,8 @@ impl GitPanel {
                                 }
 
                                 items
-                            }
-                        })
+                            }),
+                        )
                         .when(
                             !self.horizontal_scrollbar.show_track
                                 && self.horizontal_scrollbar.show_scrollbar,

crates/gpui/examples/data_table.rs 🔗

@@ -378,8 +378,6 @@ impl DataTable {
 
 impl Render for DataTable {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let entity = cx.entity();
-
         div()
             .font_family(".SystemUIFont")
             .bg(gpui::white())
@@ -431,8 +429,10 @@ impl Render for DataTable {
                             .relative()
                             .size_full()
                             .child(
-                                uniform_list(entity, "items", self.quotes.len(), {
-                                    move |this, range, _, _| {
+                                uniform_list(
+                                    "items",
+                                    self.quotes.len(),
+                                    cx.processor(move |this, range: Range<usize>, _, _| {
                                         this.visible_range = range.clone();
                                         let mut items = Vec::with_capacity(range.end - range.start);
                                         for i in range {
@@ -441,8 +441,8 @@ impl Render for DataTable {
                                             }
                                         }
                                         items
-                                    }
-                                })
+                                    }),
+                                )
                                 .size_full()
                                 .track_scroll(self.scroll_handle.clone()),
                             )

crates/gpui/examples/uniform_list.rs 🔗

@@ -9,10 +9,9 @@ impl Render for UniformListExample {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         div().size_full().bg(rgb(0xffffff)).child(
             uniform_list(
-                cx.entity().clone(),
                 "entries",
                 50,
-                |_this, range, _window, _cx| {
+                cx.processor(|_this, range, _window, _cx| {
                     let mut items = Vec::new();
                     for ix in range {
                         let item = ix + 1;
@@ -29,7 +28,7 @@ impl Render for UniformListExample {
                         );
                     }
                     items
-                },
+                }),
             )
             .h_full(),
         )

crates/gpui/src/app/context.rs 🔗

@@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> {
         }
     }
 
+    /// Convenience method for producing view state in a closure.
+    /// See `listener` for more details.
+    pub fn processor<E, R>(
+        &self,
+        f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
+    ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
+        let view = self.entity();
+        move |e: E, window: &mut Window, cx: &mut App| {
+            view.update(cx, |view, cx| f(view, e, window, cx))
+        }
+    }
+
     /// Run something using this entity and cx, when the returned struct is dropped
     pub fn on_drop(
         &self,

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

@@ -5,10 +5,10 @@
 //! elements with uniform height.
 
 use crate::{
-    AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
-    GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
-    IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
-    StyleRefinement, Styled, Window, point, size,
+    AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
+    Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
+    ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
+    point, size,
 };
 use smallvec::SmallVec;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior;
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 /// uniform_list will only render the visible subset of items.
 #[track_caller]
-pub fn uniform_list<I, R, V>(
-    view: Entity<V>,
-    id: I,
+pub fn uniform_list<R>(
+    id: impl Into<ElementId>,
     item_count: usize,
-    f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
+    f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
 ) -> UniformList
 where
-    I: Into<ElementId>,
     R: IntoElement,
-    V: Render,
 {
     let id = id.into();
     let mut base_style = StyleRefinement::default();
     base_style.overflow.y = Some(Overflow::Scroll);
 
-    let render_range = move |range, window: &mut Window, cx: &mut App| {
-        view.update(cx, |this, cx| {
-            f(this, range, window, cx)
-                .into_iter()
-                .map(|component| component.into_any_element())
-                .collect()
-        })
+    let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
+        f(range, window, cx)
+            .into_iter()
+            .map(|component| component.into_any_element())
+            .collect()
     };
 
     UniformList {

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -303,10 +303,9 @@ impl Render for SyntaxTreeView {
         {
             let layer = layer.clone();
             rendered = rendered.child(uniform_list(
-                cx.entity().clone(),
                 "SyntaxTreeView",
                 layer.node().descendant_count(),
-                move |this, range, _, cx| {
+                cx.processor(move |this, range: Range<usize>, _, cx| {
                     let mut items = Vec::new();
                     let mut cursor = layer.node().walk();
                     let mut descendant_ix = range.start;
@@ -377,7 +376,7 @@ impl Render for SyntaxTreeView {
                         }
                     }
                     items
-                },
+                }),
             )
             .size_full()
             .track_scroll(self.list_scroll_handle.clone())

crates/outline_panel/src/outline_panel.rs 🔗

@@ -4497,8 +4497,10 @@ impl OutlinePanel {
                 let multi_buffer_snapshot = self
                     .active_editor()
                     .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
-                uniform_list(cx.entity().clone(), "entries", items_len, {
-                    move |outline_panel, range, window, cx| {
+                uniform_list(
+                    "entries",
+                    items_len,
+                    cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
                         let entries = outline_panel.cached_entries.get(range);
                         entries
                             .map(|entries| entries.to_vec())
@@ -4555,8 +4557,8 @@ impl OutlinePanel {
                                 ),
                             })
                             .collect()
-                    }
-                })
+                    }),
+                )
                 .with_sizing_behavior(ListSizingBehavior::Infer)
                 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
                 .with_width_from_item(self.max_width_item_index)

crates/picker/src/picker.rs 🔗

@@ -17,7 +17,7 @@ use gpui::{
 use head::Head;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use std::{sync::Arc, time::Duration};
+use std::{ops::Range, sync::Arc, time::Duration};
 use ui::{
     Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex,
 };
@@ -760,14 +760,13 @@ impl<D: PickerDelegate> Picker<D> {
 
         match &self.element_container {
             ElementContainer::UniformList(scroll_handle) => uniform_list(
-                cx.entity().clone(),
                 "candidates",
                 self.delegate.match_count(),
-                move |picker, visible_range, window, cx| {
+                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
                     visible_range
                         .map(|ix| picker.render_element(window, cx, ix))
                         .collect()
-                },
+                }),
             )
             .with_sizing_behavior(sizing_behavior)
             .when_some(self.widest_item, |el, widest_item| {

crates/project_panel/src/project_panel.rs 🔗

@@ -4914,8 +4914,8 @@ impl Render for ProjectPanel {
                 )
                 .track_focus(&self.focus_handle(cx))
                 .child(
-                    uniform_list(cx.entity().clone(), "entries", item_count, {
-                        |this, range, window, cx| {
+                    uniform_list("entries", item_count, {
+                        cx.processor(|this, range: Range<usize>, window, cx| {
                             let mut items = Vec::with_capacity(range.end - range.start);
                             this.for_each_visible_entry(
                                 range,
@@ -4926,7 +4926,7 @@ impl Render for ProjectPanel {
                                 },
                             );
                             items
-                        }
+                        })
                     })
                     .when(show_indent_guides, |list| {
                         list.with_decoration(

crates/semantic_index/src/project_index_debug_view.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 };
 use project::WorktreeId;
 use settings::Settings;
-use std::{path::Path, sync::Arc};
+use std::{ops::Range, path::Path, sync::Arc};
 use theme::ThemeSettings;
 use ui::prelude::*;
 use workspace::item::Item;
@@ -224,10 +224,9 @@ impl Render for ProjectIndexDebugView {
                 .into_any_element()
         } else {
             let mut list = uniform_list(
-                cx.entity().clone(),
                 "ProjectIndexDebugView",
                 self.rows.len(),
-                move |this, range, _, cx| {
+                cx.processor(move |this, range: Range<usize>, _, cx| {
                     this.rows[range]
                         .iter()
                         .enumerate()
@@ -262,7 +261,7 @@ impl Render for ProjectIndexDebugView {
                                 })),
                         })
                         .collect()
-                },
+                }),
             )
             .track_scroll(self.list_scroll_handle.clone())
             .size_full()

crates/zed/src/zed/component_preview.rs 🔗

@@ -5,6 +5,7 @@
 mod persistence;
 mod preview_support;
 
+use std::ops::Range;
 use std::sync::Arc;
 
 use std::iter::Iterator;
@@ -780,10 +781,9 @@ impl Render for ComponentPreview {
                     .h_full()
                     .child(
                         gpui::uniform_list(
-                            cx.entity().clone(),
                             "component-nav",
                             sidebar_entries.len(),
-                            move |this, range, _window, cx| {
+                            cx.processor(move |this, range: Range<usize>, _window, cx| {
                                 range
                                     .filter_map(|ix| {
                                         if ix < sidebar_entries.len() {
@@ -797,7 +797,7 @@ impl Render for ComponentPreview {
                                         }
                                     })
                                     .collect()
-                            },
+                            }),
                         )
                         .track_scroll(self.nav_scroll_handle.clone())
                         .pt_4()