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 mention_set = mention_set.clone();
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0), { 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)
})
}
});
Self { Self {
agent: agent.clone(), agent: agent.clone(),
@ -2552,10 +2536,21 @@ impl Render for AcpThreadView {
v_flex().flex_1().map(|this| { v_flex().flex_1().map(|this| {
if self.list_state.item_count() > 0 { if self.list_state.item_count() > 0 {
this.child( this.child(
list(self.list_state.clone()) list(
.with_sizing_behavior(gpui::ListSizingBehavior::Auto) self.list_state.clone(),
.flex_grow() cx.processor(|this, index: usize, window, cx| {
.into_any(), 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(),
) )
.children(match thread_clone.read(cx).status() { .children(match thread_clone.read(cx).status() {
ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {

View file

@ -780,13 +780,7 @@ impl ActiveThread {
cx.observe_global::<SettingsStore>(|_, cx| cx.notify()), cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
]; ];
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), { 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 workspace_subscription = if let Some(workspace) = workspace.upgrade() { let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
Some(cx.observe_release(&workspace, |this, _, cx| { 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 message_id = self.messages[ix];
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
@ -3613,7 +3612,7 @@ impl Render for ActiveThread {
this.hide_scrollbar_later(cx); 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| { .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
this.child(scrollbar) this.child(scrollbar)
}) })

View file

@ -1471,7 +1471,6 @@ impl AgentPanel {
let current_is_special = current_is_history || current_is_config; let current_is_special = current_is_history || current_is_config;
let new_is_special = new_is_history || new_is_config; let new_is_special = new_is_history || new_is_config;
let mut old_acp_thread = None;
match &self.active_view { match &self.active_view {
ActiveView::Thread { thread, .. } => { 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; 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.acp_message_history.borrow_mut().reset_position();
self.focus_handle(cx).focus(window); self.focus_handle(cx).focus(window);

View file

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

View file

@ -324,20 +324,6 @@ impl CollabPanel {
) )
.detach(); .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 { let mut this = Self {
width: None, width: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -345,7 +331,7 @@ impl CollabPanel {
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
context_menu: None, context_menu: None,
list_state, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
channel_name_editor, channel_name_editor,
filter_editor, filter_editor,
entries: Vec::default(), entries: Vec::default(),
@ -2431,7 +2417,13 @@ impl CollabPanel {
}); });
v_flex() v_flex()
.size_full() .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( .child(
v_flex() v_flex()
.child(div().mx_2().border_primary(cx).border_t_1()) .child(div().mx_2().border_primary(cx).border_t_1())

View file

@ -118,16 +118,7 @@ impl NotificationPanel {
}) })
.detach(); .detach();
let entity = cx.entity().downgrade(); let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
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())
});
notification_list.set_scroll_handler(cx.listener( notification_list.set_scroll_handler(cx.listener(
|this, event: &ListScrollEvent, _, cx| { |this, event: &ListScrollEvent, _, cx| {
if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD { if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
@ -687,7 +678,16 @@ impl Render for NotificationPanel {
), ),
) )
} else { } 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 { impl LoadedSourceList {
pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self { pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
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 _subscription = cx.subscribe(&session, |this, _, event, cx| match event { let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::LoadedSources => { SessionEvent::Stopped(_) | SessionEvent::LoadedSources => {
@ -98,6 +84,12 @@ impl Render for LoadedSourceList {
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.size_full() .size_full()
.p_1() .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 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 scrollbar_state = ScrollbarState::new(list_state.clone()); let scrollbar_state = ScrollbarState::new(list_state.clone());
let mut this = Self { let mut this = Self {
@ -708,11 +702,14 @@ impl StackFrameList {
self.activate_selected_entry(window, cx); self.activate_selected_entry(window, cx);
} }
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div() div().p_1().size_full().child(
.p_1() list(
.size_full() self.list_state.clone(),
.child(list(self.list_state.clone()).size_full()) 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 std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, Dimensions, SumTree}; use sum_tree::{Bias, Dimensions, SumTree};
type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static;
/// Construct a new list element /// 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 { List {
state, state,
render_item: Box::new(render_item),
style: StyleRefinement::default(), style: StyleRefinement::default(),
sizing_behavior: ListSizingBehavior::default(), sizing_behavior: ListSizingBehavior::default(),
} }
@ -30,6 +36,7 @@ pub fn list(state: ListState) -> List {
/// A list element /// A list element
pub struct List { pub struct List {
state: ListState, state: ListState,
render_item: Box<RenderItemFn>,
style: StyleRefinement, style: StyleRefinement,
sizing_behavior: ListSizingBehavior, sizing_behavior: ListSizingBehavior,
} }
@ -55,7 +62,6 @@ impl std::fmt::Debug for ListState {
struct StateInner { struct StateInner {
last_layout_bounds: Option<Bounds<Pixels>>, last_layout_bounds: Option<Bounds<Pixels>>,
last_padding: Option<Edges<Pixels>>, last_padding: Option<Edges<Pixels>>,
render_item: Box<dyn FnMut(usize, &mut Window, &mut App) -> AnyElement>,
items: SumTree<ListItem>, items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>, logical_scroll_top: Option<ListOffset>,
alignment: ListAlignment, alignment: ListAlignment,
@ -186,19 +192,10 @@ impl ListState {
/// above and below the visible area. Elements within this area will /// above and below the visible area. Elements within this area will
/// be measured even though they are not visible. This can help ensure /// be measured even though they are not visible. This can help ensure
/// that the list doesn't flicker or pop in when scrolling. /// that the list doesn't flicker or pop in when scrolling.
pub fn new<R>( pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self {
item_count: usize,
alignment: ListAlignment,
overdraw: Pixels,
render_item: R,
) -> Self
where
R: 'static + FnMut(usize, &mut Window, &mut App) -> AnyElement,
{
let this = Self(Rc::new(RefCell::new(StateInner { let this = Self(Rc::new(RefCell::new(StateInner {
last_layout_bounds: None, last_layout_bounds: None,
last_padding: None, last_padding: None,
render_item: Box::new(render_item),
items: SumTree::default(), items: SumTree::default(),
logical_scroll_top: None, logical_scroll_top: None,
alignment, alignment,
@ -532,6 +529,7 @@ impl StateInner {
available_width: Option<Pixels>, available_width: Option<Pixels>,
available_height: Pixels, available_height: Pixels,
padding: &Edges<Pixels>, padding: &Edges<Pixels>,
render_item: &mut RenderItemFn,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> LayoutItemsResponse { ) -> 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 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() { if visible_height < available_height || size.is_none() {
let item_index = scroll_top.item_ix + ix; 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); let element_size = element.layout_as_root(available_item_space, window, cx);
size = Some(element_size); size = Some(element_size);
if visible_height < available_height { if visible_height < available_height {
@ -601,7 +599,7 @@ impl StateInner {
cursor.prev(); cursor.prev();
if let Some(item) = cursor.item() { if let Some(item) = cursor.item() {
let item_index = cursor.start().0; 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 element_size = element.layout_as_root(available_item_space, window, cx);
let focus_handle = item.focus_handle(); let focus_handle = item.focus_handle();
rendered_height += element_size.height; rendered_height += element_size.height;
@ -650,7 +648,7 @@ impl StateInner {
let size = if let ListItem::Measured { size, .. } = item { let size = if let ListItem::Measured { size, .. } = item {
*size *size
} else { } 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) element.layout_as_root(available_item_space, window, cx)
}; };
@ -683,7 +681,7 @@ impl StateInner {
while let Some(item) = cursor.item() { while let Some(item) = cursor.item() {
if item.contains_focused(window, cx) { if item.contains_focused(window, cx) {
let item_index = cursor.start().0; 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); let size = element.layout_as_root(available_item_space, window, cx);
item_layouts.push_back(ItemLayout { item_layouts.push_back(ItemLayout {
index: item_index, index: item_index,
@ -708,6 +706,7 @@ impl StateInner {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
padding: Edges<Pixels>, padding: Edges<Pixels>,
autoscroll: bool, autoscroll: bool,
render_item: &mut RenderItemFn,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Result<LayoutItemsResponse, ListOffset> { ) -> Result<LayoutItemsResponse, ListOffset> {
@ -716,6 +715,7 @@ impl StateInner {
Some(bounds.size.width), Some(bounds.size.width),
bounds.size.height, bounds.size.height,
&padding, &padding,
render_item,
window, window,
cx, cx,
); );
@ -753,8 +753,7 @@ impl StateInner {
let Some(item) = cursor.item() else { break }; let Some(item) = cursor.item() else { break };
let size = item.size().unwrap_or_else(|| { let size = item.size().unwrap_or_else(|| {
let mut item = let mut item = render_item(cursor.start().0, window, cx);
(self.render_item)(cursor.start().0, window, cx);
let item_available_size = size( let item_available_size = size(
bounds.size.width.into(), bounds.size.width.into(),
AvailableSpace::MinContent, AvailableSpace::MinContent,
@ -876,8 +875,14 @@ impl Element for List {
window.rem_size(), window.rem_size(),
); );
let layout_response = let layout_response = state.layout_items(
state.layout_items(None, available_height, &padding, window, cx); None,
available_height,
&padding,
&mut self.render_item,
window,
cx,
);
let max_element_width = layout_response.max_item_width; let max_element_width = layout_response.max_item_width;
let summary = state.items.summary(); let summary = state.items.summary();
@ -951,15 +956,16 @@ impl Element for List {
let padding = style let padding = style
.padding .padding
.to_pixels(bounds.size.into(), window.rem_size()); .to_pixels(bounds.size.into(), window.rem_size());
let layout = match state.prepaint_items(bounds, padding, true, window, cx) { let layout =
Ok(layout) => layout, match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) {
Err(autoscroll_request) => { Ok(layout) => layout,
state.logical_scroll_top = Some(autoscroll_request); Err(autoscroll_request) => {
state state.logical_scroll_top = Some(autoscroll_request);
.prepaint_items(bounds, padding, false, window, cx) state
.unwrap() .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx)
} .unwrap()
}; }
};
state.last_layout_bounds = Some(bounds); state.last_layout_bounds = Some(bounds);
state.last_padding = Some(padding); state.last_padding = Some(padding);
@ -1108,9 +1114,7 @@ mod test {
let cx = cx.add_empty_window(); let cx = cx.add_empty_window();
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
div().h(px(10.)).w_full().into_any()
});
// Ensure that the list is scrolled to the top // Ensure that the list is scrolled to the top
state.scroll_to(gpui::ListOffset { state.scroll_to(gpui::ListOffset {
@ -1121,7 +1125,11 @@ mod test {
struct TestView(ListState); struct TestView(ListState);
impl Render for TestView { impl Render for TestView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { 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 cx = cx.add_empty_window();
let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { let state = ListState::new(5, crate::ListAlignment::Top, px(10.));
div().h(px(20.)).w_full().into_any()
});
struct TestView(ListState); struct TestView(ListState);
impl Render for TestView { impl Render for TestView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { 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 workspace::{Pane, Workspace};
use crate::markdown_elements::ParsedMarkdownElement; use crate::markdown_elements::ParsedMarkdownElement;
use crate::markdown_renderer::CheckboxClickedEvent;
use crate::{ use crate::{
MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
markdown_elements::ParsedMarkdown, markdown_elements::ParsedMarkdown,
@ -203,114 +204,7 @@ impl MarkdownPreviewView {
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) -> Entity<Self> { ) -> Entity<Self> {
cx.new(|cx| { cx.new(|cx| {
let view = cx.entity().downgrade(); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
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 mut this = Self { let mut this = Self {
selected_block: 0, selected_block: 0,
@ -607,10 +501,107 @@ impl Render for MarkdownPreviewView {
.p_4() .p_4()
.text_size(buffer_size) .text_size(buffer_size)
.line_height(relative(buffer_line_height.value())) .line_height(relative(buffer_line_height.value()))
.child( .child(div().flex_grow().map(|this| {
div() this.child(
.flex_grow() list(
.map(|this| this.child(list(self.list_state.clone()).size_full())), 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()
.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}; 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)] #[derive(Clone)]
pub struct RenderContext { pub struct RenderContext {
@ -80,7 +95,7 @@ impl RenderContext {
pub fn with_checkbox_clicked_callback( pub fn with_checkbox_clicked_callback(
mut self, mut self,
callback: impl Fn(bool, Range<usize>, &mut Window, &mut App) + 'static, callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback)));
self self
@ -229,7 +244,14 @@ fn render_markdown_list_item(
}; };
if window.modifiers().secondary() { 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, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> 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 { let scrollbar_state = match &element_container {
ElementContainer::UniformList(scroll_handle) => { ElementContainer::UniformList(scroll_handle) => {
ScrollbarState::new(scroll_handle.clone()) ScrollbarState::new(scroll_handle.clone())
@ -323,31 +323,13 @@ impl<D: PickerDelegate> Picker<D> {
this this
} }
fn create_element_container( fn create_element_container(container: ContainerKind) -> ElementContainer {
container: ContainerKind,
cx: &mut Context<Self>,
) -> ElementContainer {
match container { match container {
ContainerKind::UniformList => { ContainerKind::UniformList => {
ElementContainer::UniformList(UniformListScrollHandle::new()) ElementContainer::UniformList(UniformListScrollHandle::new())
} }
ContainerKind::List => { ContainerKind::List => {
let entity = cx.entity().downgrade(); ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
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())
},
))
} }
} }
} }
@ -786,11 +768,16 @@ impl<D: PickerDelegate> Picker<D> {
.py_1() .py_1()
.track_scroll(scroll_handle.clone()) .track_scroll(scroll_handle.clone())
.into_any_element(), .into_any_element(),
ElementContainer::List(state) => list(state.clone()) ElementContainer::List(state) => list(
.with_sizing_behavior(sizing_behavior) state.clone(),
.flex_grow() cx.processor(|this, ix, window, cx| {
.py_2() this.render_element(window, cx, ix).into_any_element()
.into_any_element(), }),
)
.with_sizing_behavior(sizing_behavior)
.flex_grow()
.py_2()
.into_any_element(),
} }
} }

View file

@ -126,29 +126,7 @@ impl NotebookEditor {
let cell_count = cell_order.len(); let cell_count = cell_order.len();
let this = cx.entity(); let this = cx.entity();
let cell_list = ListState::new( let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.));
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())
},
);
Self { Self {
project, project,
@ -544,7 +522,19 @@ impl Render for NotebookEditor {
.flex_1() .flex_1()
.size_full() .size_full()
.overflow_y_scroll() .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)) .child(self.render_notebook_controls(window, cx))
} }

View file

@ -115,21 +115,9 @@ impl ProjectIndexDebugView {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let view = cx.entity().downgrade();
this.selected_path = Some(PathState { this.selected_path = Some(PathState {
path: file_path, path: file_path,
list_state: ListState::new( list_state: ListState::new(chunks.len(), gpui::ListAlignment::Top, px(100.)),
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()
}
},
),
chunks, chunks,
}); });
cx.notify(); cx.notify();
@ -219,7 +207,13 @@ impl Render for ProjectIndexDebugView {
cx.notify(); 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() .size_full()
.into_any_element() .into_any_element()
} else { } else {

View file

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