Simplify ListState API (#35685)

Follow up to: https://github.com/zed-industries/zed/pull/35670,
simplifies the List state APIs so you no longer have to worry about
strong vs. weak pointers when rendering list items.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
Mikayla Maki 2025-08-05 17:02:26 -07:00 committed by GitHub
parent d0de81b0b4
commit 53175263a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 322 additions and 403 deletions

View file

@ -173,23 +173,7 @@ impl AcpThreadView {
let mention_set = mention_set.clone();
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), {
let this = cx.entity().downgrade();
move |index: usize, window, cx| {
let Some(this) = this.upgrade() else {
return Empty.into_any();
};
this.update(cx, |this, cx| {
let Some((entry, len)) = this.thread().and_then(|thread| {
let entries = &thread.read(cx).entries();
Some((entries.get(index)?, entries.len()))
}) else {
return Empty.into_any();
};
this.render_entry(index, len, entry, window, cx)
})
}
});
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
Self {
agent: agent.clone(),
@ -2552,7 +2536,18 @@ impl Render for AcpThreadView {
v_flex().flex_1().map(|this| {
if self.list_state.item_count() > 0 {
this.child(
list(self.list_state.clone())
list(
self.list_state.clone(),
cx.processor(|this, index: usize, window, cx| {
let Some((entry, len)) = this.thread().and_then(|thread| {
let entries = &thread.read(cx).entries();
Some((entries.get(index)?, entries.len()))
}) else {
return Empty.into_any();
};
this.render_entry(index, len, entry, window, cx)
}),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.flex_grow()
.into_any(),

View file

@ -780,13 +780,7 @@ impl ActiveThread {
cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
];
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_message(ix, window, cx))
.unwrap()
}
});
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
Some(cx.observe_release(&workspace, |this, _, cx| {
@ -1846,7 +1840,12 @@ impl ActiveThread {
)))
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
fn render_message(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let message_id = self.messages[ix];
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
@ -3613,7 +3612,7 @@ impl Render for ActiveThread {
this.hide_scrollbar_later(cx);
}),
)
.child(list(self.list_state.clone()).flex_grow())
.child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow())
.when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
this.child(scrollbar)
})

View file

@ -1471,7 +1471,6 @@ impl AgentPanel {
let current_is_special = current_is_history || current_is_config;
let new_is_special = new_is_history || new_is_config;
let mut old_acp_thread = None;
match &self.active_view {
ActiveView::Thread { thread, .. } => {
@ -1483,9 +1482,6 @@ impl AgentPanel {
});
}
}
ActiveView::ExternalAgentThread { thread_view } => {
old_acp_thread.replace(thread_view.downgrade());
}
_ => {}
}
@ -1516,11 +1512,6 @@ impl AgentPanel {
self.active_view = new_view;
}
debug_assert!(
old_acp_thread.map_or(true, |thread| !thread.is_upgradable()),
"AcpThreadView leaked"
);
self.acp_message_history.borrow_mut().reset_position();
self.focus_handle(cx).focus(window);

View file

@ -103,28 +103,16 @@ impl ChatPanel {
});
cx.new(|cx| {
let entity = cx.entity().downgrade();
let message_list = ListState::new(
0,
gpui::ListAlignment::Bottom,
px(1000.),
move |ix, window, cx| {
if let Some(entity) = entity.upgrade() {
entity.update(cx, |this: &mut Self, cx| {
this.render_message(ix, window, cx).into_any_element()
})
} else {
div().into_any()
}
},
);
let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.));
message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| {
message_list.set_scroll_handler(cx.listener(
|this: &mut Self, event: &ListScrollEvent, _, cx| {
if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
this.load_more_messages(cx);
}
this.is_scrolled_to_bottom = !event.is_scrolled;
}));
},
));
let local_offset = chrono::Local::now().offset().local_minus_utc();
let mut this = Self {
@ -399,7 +387,7 @@ impl ChatPanel {
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
) -> AnyElement {
let active_chat = &self.active_chat.as_ref().unwrap().0;
let (message, is_continuation_from_previous, is_admin) =
active_chat.update(cx, |active_chat, cx| {
@ -582,6 +570,7 @@ impl ChatPanel {
self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx)
.mt_neg_2p5(),
)
.into_any_element()
}
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
@ -979,7 +968,13 @@ impl Render for ChatPanel {
)
.child(div().flex_grow().px_2().map(|this| {
if self.active_chat.is_some() {
this.child(list(self.message_list.clone()).size_full())
this.child(
list(
self.message_list.clone(),
cx.processor(Self::render_message),
)
.size_full(),
)
} else {
this.child(
div()

View file

@ -324,20 +324,6 @@ impl CollabPanel {
)
.detach();
let entity = cx.entity().downgrade();
let list_state = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
if let Some(entity) = entity.upgrade() {
entity.update(cx, |this, cx| this.render_list_entry(ix, window, cx))
} else {
div().into_any()
}
},
);
let mut this = Self {
width: None,
focus_handle: cx.focus_handle(),
@ -345,7 +331,7 @@ impl CollabPanel {
fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None),
context_menu: None,
list_state,
list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
channel_name_editor,
filter_editor,
entries: Vec::default(),
@ -2431,7 +2417,13 @@ impl CollabPanel {
});
v_flex()
.size_full()
.child(list(self.list_state.clone()).size_full())
.child(
list(
self.list_state.clone(),
cx.processor(Self::render_list_entry),
)
.size_full(),
)
.child(
v_flex()
.child(div().mx_2().border_primary(cx).border_t_1())

View file

@ -118,16 +118,7 @@ impl NotificationPanel {
})
.detach();
let entity = cx.entity().downgrade();
let notification_list =
ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| {
entity
.upgrade()
.and_then(|entity| {
entity.update(cx, |this, cx| this.render_notification(ix, window, cx))
})
.unwrap_or_else(|| div().into_any())
});
let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
notification_list.set_scroll_handler(cx.listener(
|this, event: &ListScrollEvent, _, cx| {
if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
@ -687,7 +678,16 @@ impl Render for NotificationPanel {
),
)
} else {
this.child(list(self.notification_list.clone()).size_full())
this.child(
list(
self.notification_list.clone(),
cx.processor(|this, ix, window, cx| {
this.render_notification(ix, window, cx)
.unwrap_or_else(|| div().into_any())
}),
)
.size_full(),
)
}
})
}

View file

@ -13,22 +13,8 @@ pub(crate) struct LoadedSourceList {
impl LoadedSourceList {
pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|loaded_sources| {
loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx))
})
.unwrap_or(div().into_any())
},
);
let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::LoadedSources => {
@ -98,6 +84,12 @@ impl Render for LoadedSourceList {
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
.child(
list(
self.list.clone(),
cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
)
.size_full(),
)
}
}

View file

@ -70,13 +70,7 @@ impl StackFrameList {
_ => {}
});
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
let this = cx.weak_entity();
move |ix, _window, cx| {
this.update(cx, |this, cx| this.render_entry(ix, cx))
.unwrap_or(div().into_any())
}
});
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let scrollbar_state = ScrollbarState::new(list_state.clone());
let mut this = Self {
@ -708,11 +702,14 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.p_1()
.size_full()
.child(list(self.list_state.clone()).size_full())
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().p_1().size_full().child(
list(
self.list_state.clone(),
cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
)
.size_full(),
)
}
}

View file

@ -18,10 +18,16 @@ use refineable::Refineable as _;
use std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, Dimensions, SumTree};
type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
/// Construct a new list element
pub fn list(state: ListState) -> List {
pub fn list(
state: ListState,
render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static,
) -> List {
List {
state,
render_item: Box::new(render_item),
style: StyleRefinement::default(),
sizing_behavior: ListSizingBehavior::default(),
}
@ -30,6 +36,7 @@ pub fn list(state: ListState) -> List {
/// A list element
pub struct List {
state: ListState,
render_item: Box<RenderItemFn>,
style: StyleRefinement,
sizing_behavior: ListSizingBehavior,
}
@ -55,7 +62,6 @@ impl std::fmt::Debug for ListState {
struct StateInner {
last_layout_bounds: Option<Bounds<Pixels>>,
last_padding: Option<Edges<Pixels>>,
render_item: Box<dyn FnMut(usize, &mut Window, &mut App) -> AnyElement>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
alignment: ListAlignment,
@ -186,19 +192,10 @@ impl ListState {
/// above and below the visible area. Elements within this area will
/// be measured even though they are not visible. This can help ensure
/// that the list doesn't flicker or pop in when scrolling.
pub fn new<R>(
item_count: usize,
alignment: ListAlignment,
overdraw: Pixels,
render_item: R,
) -> Self
where
R: 'static + FnMut(usize, &mut Window, &mut App) -> AnyElement,
{
pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
let this = Self(Rc::new(RefCell::new(StateInner {
last_layout_bounds: None,
last_padding: None,
render_item: Box::new(render_item),
items: SumTree::default(),
logical_scroll_top: None,
alignment,
@ -532,6 +529,7 @@ impl StateInner {
available_width: Option<Pixels>,
available_height: Pixels,
padding: &Edges<Pixels>,
render_item: &mut RenderItemFn,
window: &mut Window,
cx: &mut App,
) -> LayoutItemsResponse {
@ -566,7 +564,7 @@ impl StateInner {
// If we're within the visible area or the height wasn't cached, render and measure the item's element
if visible_height < available_height || size.is_none() {
let item_index = scroll_top.item_ix + ix;
let mut element = (self.render_item)(item_index, window, cx);
let mut element = render_item(item_index, window, cx);
let element_size = element.layout_as_root(available_item_space, window, cx);
size = Some(element_size);
if visible_height < available_height {
@ -601,7 +599,7 @@ impl StateInner {
cursor.prev();
if let Some(item) = cursor.item() {
let item_index = cursor.start().0;
let mut element = (self.render_item)(item_index, window, cx);
let mut element = render_item(item_index, window, cx);
let element_size = element.layout_as_root(available_item_space, window, cx);
let focus_handle = item.focus_handle();
rendered_height += element_size.height;
@ -650,7 +648,7 @@ impl StateInner {
let size = if let ListItem::Measured { size, .. } = item {
*size
} else {
let mut element = (self.render_item)(cursor.start().0, window, cx);
let mut element = render_item(cursor.start().0, window, cx);
element.layout_as_root(available_item_space, window, cx)
};
@ -683,7 +681,7 @@ impl StateInner {
while let Some(item) = cursor.item() {
if item.contains_focused(window, cx) {
let item_index = cursor.start().0;
let mut element = (self.render_item)(cursor.start().0, window, cx);
let mut element = render_item(cursor.start().0, window, cx);
let size = element.layout_as_root(available_item_space, window, cx);
item_layouts.push_back(ItemLayout {
index: item_index,
@ -708,6 +706,7 @@ impl StateInner {
bounds: Bounds<Pixels>,
padding: Edges<Pixels>,
autoscroll: bool,
render_item: &mut RenderItemFn,
window: &mut Window,
cx: &mut App,
) -> Result<LayoutItemsResponse, ListOffset> {
@ -716,6 +715,7 @@ impl StateInner {
Some(bounds.size.width),
bounds.size.height,
&padding,
render_item,
window,
cx,
);
@ -753,8 +753,7 @@ impl StateInner {
let Some(item) = cursor.item() else { break };
let size = item.size().unwrap_or_else(|| {
let mut item =
(self.render_item)(cursor.start().0, window, cx);
let mut item = render_item(cursor.start().0, window, cx);
let item_available_size = size(
bounds.size.width.into(),
AvailableSpace::MinContent,
@ -876,8 +875,14 @@ impl Element for List {
window.rem_size(),
);
let layout_response =
state.layout_items(None, available_height, &padding, window, cx);
let layout_response = state.layout_items(
None,
available_height,
&padding,
&mut self.render_item,
window,
cx,
);
let max_element_width = layout_response.max_item_width;
let summary = state.items.summary();
@ -951,12 +956,13 @@ impl Element for List {
let padding = style
.padding
.to_pixels(bounds.size.into(), window.rem_size());
let layout = match state.prepaint_items(bounds, padding, true, window, cx) {
let layout =
match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
Ok(layout) => layout,
Err(autoscroll_request) => {
state.logical_scroll_top = Some(autoscroll_request);
state
.prepaint_items(bounds, padding, false, window, cx)
.prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
.unwrap()
}
};
@ -1108,9 +1114,7 @@ mod test {
let cx = cx.add_empty_window();
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| {
div().h(px(10.)).w_full().into_any()
});
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
// Ensure that the list is scrolled to the top
state.scroll_to(gpui::ListOffset {
@ -1121,7 +1125,11 @@ mod test {
struct TestView(ListState);
impl Render for TestView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
list(self.0.clone()).w_full().h_full()
list(self.0.clone(), |_, _, _| {
div().h(px(10.)).w_full().into_any()
})
.w_full()
.h_full()
}
}
@ -1154,14 +1162,16 @@ mod test {
let cx = cx.add_empty_window();
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| {
div().h(px(20.)).w_full().into_any()
});
let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
struct TestView(ListState);
impl Render for TestView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
list(self.0.clone()).w_full().h_full()
list(self.0.clone(), |_, _, _| {
div().h(px(20.)).w_full().into_any()
})
.w_full()
.h_full()
}
}

View file

@ -18,6 +18,7 @@ use workspace::item::{Item, ItemHandle};
use workspace::{Pane, Workspace};
use crate::markdown_elements::ParsedMarkdownElement;
use crate::markdown_renderer::CheckboxClickedEvent;
use crate::{
MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
markdown_elements::ParsedMarkdown,
@ -203,114 +204,7 @@ impl MarkdownPreviewView {
cx: &mut Context<Workspace>,
) -> Entity<Self> {
cx.new(|cx| {
let view = cx.entity().downgrade();
let list_state = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |this: &mut Self, cx| {
let Some(contents) = &this.contents else {
return div().into_any();
};
let mut render_cx =
RenderContext::new(Some(this.workspace.clone()), window, cx)
.with_checkbox_clicked_callback({
let view = view.clone();
move |checked, source_range, window, cx| {
view.update(cx, |view, cx| {
if let Some(editor) = view
.active_editor
.as_ref()
.map(|s| s.editor.clone())
{
editor.update(cx, |editor, cx| {
let task_marker =
if checked { "[x]" } else { "[ ]" };
editor.edit(
vec![(source_range, task_marker)],
cx,
);
});
view.parse_markdown_from_active_editor(
false, window, cx,
);
cx.notify();
}
})
}
});
let block = contents.children.get(ix).unwrap();
let rendered_block = render_markdown_block(block, &mut render_cx);
let should_apply_padding = Self::should_apply_padding_between(
block,
contents.children.get(ix + 1),
);
div()
.id(ix)
.when(should_apply_padding, |this| {
this.pb(render_cx.scaled_rems(0.75))
})
.group("markdown-block")
.on_click(cx.listener(
move |this, event: &ClickEvent, window, cx| {
if event.click_count() == 2 {
if let Some(source_range) = this
.contents
.as_ref()
.and_then(|c| c.children.get(ix))
.and_then(|block| block.source_range())
{
this.move_cursor_to_block(
window,
cx,
source_range.start..source_range.start,
);
}
}
},
))
.map(move |container| {
let indicator = div()
.h_full()
.w(px(4.0))
.when(ix == this.selected_block, |this| {
this.bg(cx.theme().colors().border)
})
.group_hover("markdown-block", |s| {
if ix == this.selected_block {
s
} else {
s.bg(cx.theme().colors().border_variant)
}
})
.rounded_xs();
container.child(
div()
.relative()
.child(
div()
.pl(render_cx.scaled_rems(1.0))
.child(rendered_block),
)
.child(indicator.absolute().left_0().top_0()),
)
})
.into_any()
})
} else {
div().into_any()
}
},
);
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let mut this = Self {
selected_block: 0,
@ -607,10 +501,107 @@ impl Render for MarkdownPreviewView {
.p_4()
.text_size(buffer_size)
.line_height(relative(buffer_line_height.value()))
.child(div().flex_grow().map(|this| {
this.child(
list(
self.list_state.clone(),
cx.processor(|this, ix, window, cx| {
let Some(contents) = &this.contents else {
return div().into_any();
};
let mut render_cx =
RenderContext::new(Some(this.workspace.clone()), window, cx)
.with_checkbox_clicked_callback(cx.listener(
move |this, e: &CheckboxClickedEvent, window, cx| {
if let Some(editor) = this
.active_editor
.as_ref()
.map(|s| s.editor.clone())
{
editor.update(cx, |editor, cx| {
let task_marker =
if e.checked() { "[x]" } else { "[ ]" };
editor.edit(
vec![(e.source_range(), task_marker)],
cx,
);
});
this.parse_markdown_from_active_editor(
false, window, cx,
);
cx.notify();
}
},
));
let block = contents.children.get(ix).unwrap();
let rendered_block = render_markdown_block(block, &mut render_cx);
let should_apply_padding = Self::should_apply_padding_between(
block,
contents.children.get(ix + 1),
);
div()
.id(ix)
.when(should_apply_padding, |this| {
this.pb(render_cx.scaled_rems(0.75))
})
.group("markdown-block")
.on_click(cx.listener(
move |this, event: &ClickEvent, window, cx| {
if event.click_count() == 2 {
if let Some(source_range) = this
.contents
.as_ref()
.and_then(|c| c.children.get(ix))
.and_then(|block: &ParsedMarkdownElement| {
block.source_range()
})
{
this.move_cursor_to_block(
window,
cx,
source_range.start..source_range.start,
);
}
}
},
))
.map(move |container| {
let indicator = div()
.h_full()
.w(px(4.0))
.when(ix == this.selected_block, |this| {
this.bg(cx.theme().colors().border)
})
.group_hover("markdown-block", |s| {
if ix == this.selected_block {
s
} else {
s.bg(cx.theme().colors().border_variant)
}
})
.rounded_xs();
container.child(
div()
.relative()
.child(
div()
.flex_grow()
.map(|this| this.child(list(self.list_state.clone()).size_full())),
.pl(render_cx.scaled_rems(1.0))
.child(rendered_block),
)
.child(indicator.absolute().left_0().top_0()),
)
})
.into_any()
}),
)
.size_full(),
)
}))
}
}

View file

@ -26,7 +26,22 @@ use ui::{
};
use workspace::{OpenOptions, OpenVisible, Workspace};
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut Window, &mut App)>>;
pub struct CheckboxClickedEvent {
pub checked: bool,
pub source_range: Range<usize>,
}
impl CheckboxClickedEvent {
pub fn source_range(&self) -> Range<usize> {
self.source_range.clone()
}
pub fn checked(&self) -> bool {
self.checked
}
}
type CheckboxClickedCallback = Arc<Box<dyn Fn(&CheckboxClickedEvent, &mut Window, &mut App)>>;
#[derive(Clone)]
pub struct RenderContext {
@ -80,7 +95,7 @@ impl RenderContext {
pub fn with_checkbox_clicked_callback(
mut self,
callback: impl Fn(bool, Range<usize>, &mut Window, &mut App) + 'static,
callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback)));
self
@ -229,7 +244,14 @@ fn render_markdown_list_item(
};
if window.modifiers().secondary() {
callback(checked, range.clone(), window, cx);
callback(
&CheckboxClickedEvent {
checked,
source_range: range.clone(),
},
window,
cx,
);
}
}
})

View file

@ -292,7 +292,7 @@ impl<D: PickerDelegate> Picker<D> {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let element_container = Self::create_element_container(container, cx);
let element_container = Self::create_element_container(container);
let scrollbar_state = match &element_container {
ElementContainer::UniformList(scroll_handle) => {
ScrollbarState::new(scroll_handle.clone())
@ -323,31 +323,13 @@ impl<D: PickerDelegate> Picker<D> {
this
}
fn create_element_container(
container: ContainerKind,
cx: &mut Context<Self>,
) -> ElementContainer {
fn create_element_container(container: ContainerKind) -> ElementContainer {
match container {
ContainerKind::UniformList => {
ElementContainer::UniformList(UniformListScrollHandle::new())
}
ContainerKind::List => {
let entity = cx.entity().downgrade();
ElementContainer::List(ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
entity
.upgrade()
.map(|entity| {
entity.update(cx, |this, cx| {
this.render_element(window, cx, ix).into_any_element()
})
})
.unwrap_or_else(|| div().into_any_element())
},
))
ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
}
}
}
@ -786,7 +768,12 @@ impl<D: PickerDelegate> Picker<D> {
.py_1()
.track_scroll(scroll_handle.clone())
.into_any_element(),
ElementContainer::List(state) => list(state.clone())
ElementContainer::List(state) => list(
state.clone(),
cx.processor(|this, ix, window, cx| {
this.render_element(window, cx, ix).into_any_element()
}),
)
.with_sizing_behavior(sizing_behavior)
.flex_grow()
.py_2()

View file

@ -126,29 +126,7 @@ impl NotebookEditor {
let cell_count = cell_order.len();
let this = cx.entity();
let cell_list = ListState::new(
cell_count,
gpui::ListAlignment::Top,
px(1000.),
move |ix, window, cx| {
notebook_handle
.upgrade()
.and_then(|notebook_handle| {
notebook_handle.update(cx, |notebook, cx| {
notebook
.cell_order
.get(ix)
.and_then(|cell_id| notebook.cell_map.get(cell_id))
.map(|cell| {
notebook
.render_cell(ix, cell, window, cx)
.into_any_element()
})
})
})
.unwrap_or_else(|| div().into_any())
},
);
let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
Self {
project,
@ -544,7 +522,19 @@ impl Render for NotebookEditor {
.flex_1()
.size_full()
.overflow_y_scroll()
.child(list(self.cell_list.clone()).size_full()),
.child(list(
self.cell_list.clone(),
cx.processor(|this, ix, window, cx| {
this.cell_order
.get(ix)
.and_then(|cell_id| this.cell_map.get(cell_id))
.map(|cell| {
this.render_cell(ix, cell, window, cx).into_any_element()
})
.unwrap_or_else(|| div().into_any())
}),
))
.size_full(),
)
.child(self.render_notebook_controls(window, cx))
}

View file

@ -115,21 +115,9 @@ impl ProjectIndexDebugView {
.collect::<Vec<_>>();
this.update(cx, |this, cx| {
let view = cx.entity().downgrade();
this.selected_path = Some(PathState {
path: file_path,
list_state: ListState::new(
chunks.len(),
gpui::ListAlignment::Top,
px(100.),
move |ix, _, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| view.render_chunk(ix, cx))
} else {
div().into_any()
}
},
),
list_state: ListState::new(chunks.len(), gpui::ListAlignment::Top, px(100.)),
chunks,
});
cx.notify();
@ -219,7 +207,13 @@ impl Render for ProjectIndexDebugView {
cx.notify();
})),
)
.child(list(selected_path.list_state.clone()).size_full())
.child(
list(
selected_path.list_state.clone(),
cx.processor(|this, ix, _, cx| this.render_chunk(ix, cx)),
)
.size_full(),
)
.size_full()
.into_any_element()
} else {

View file

@ -107,6 +107,7 @@ struct ComponentPreview {
active_thread: Option<Entity<ActiveThread>>,
reset_key: usize,
component_list: ListState,
entries: Vec<PreviewEntry>,
component_map: HashMap<ComponentId, ComponentMetadata>,
components: Vec<ComponentMetadata>,
cursor_index: usize,
@ -172,17 +173,6 @@ impl ComponentPreview {
sorted_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| {
let component = this.get_component(ix);
this.render_preview(&component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let mut component_preview = Self {
@ -190,6 +180,7 @@ impl ComponentPreview {
active_thread: None,
reset_key: 0,
component_list,
entries: Vec::new(),
component_map: component_registry.component_map(),
components: sorted_components,
cursor_index: selected_index,
@ -276,10 +267,6 @@ impl ComponentPreview {
cx.notify();
}
fn get_component(&self, ix: usize) -> ComponentMetadata {
self.components[ix].clone()
}
fn filtered_components(&self) -> Vec<ComponentMetadata> {
if self.filter_text.is_empty() {
return self.components.clone();
@ -420,7 +407,6 @@ impl ComponentPreview {
fn update_component_list(&mut self, cx: &mut Context<Self>) {
let entries = self.scope_ordered_entries();
let new_len = entries.len();
let weak_entity = cx.entity().downgrade();
if new_len > 0 {
self.nav_scroll_handle
@ -446,56 +432,9 @@ impl ComponentPreview {
}
}
self.component_list = ListState::new(
filtered_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let components = filtered_components.clone();
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
if ix >= components.len() {
return div().w_full().h_0().into_any_element();
}
self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0));
self.entries = entries;
this.update(cx, |this, cx| {
let component = &components[ix];
this.render_preview(component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
let new_list = ListState::new(
new_len,
gpui::ListAlignment::Top,
px(1500.0),
move |ix, window, cx| {
if ix >= entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &entries[ix];
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
);
self.component_list = new_list;
cx.emit(ItemEvent::UpdateTab);
}
@ -672,7 +611,32 @@ impl ComponentPreview {
.child(format!("No components matching '{}'.", self.filter_text))
.into_any_element()
} else {
list(self.component_list.clone())
list(
self.component_list.clone(),
cx.processor(|this, ix, window, cx| {
if ix >= this.entries.len() {
return div().w_full().h_0().into_any_element();
}
let entry = &this.entries[ix];
match entry {
PreviewEntry::Component(component, _) => this
.render_preview(component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => {
div().w_full().h_0().into_any_element()
}
PreviewEntry::ActiveThread => {
div().w_full().h_0().into_any_element()
}
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
}
}),
)
.flex_grow()
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.into_any_element()