From 53175263a1b4bb6ce99f4b0cadc06476c4e9624f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 5 Aug 2025 17:02:26 -0700 Subject: [PATCH] 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 --- crates/agent_ui/src/acp/thread_view.rs | 37 ++- crates/agent_ui/src/active_thread.rs | 17 +- crates/agent_ui/src/agent_panel.rs | 9 - crates/collab_ui/src/chat_panel.rs | 41 ++-- crates/collab_ui/src/collab_panel.rs | 24 +- crates/collab_ui/src/notification_panel.rs | 22 +- .../src/session/running/loaded_source_list.rs | 24 +- .../src/session/running/stack_frame_list.rs | 21 +- crates/gpui/src/elements/list.rs | 84 ++++--- .../src/markdown_preview_view.rs | 217 +++++++++--------- .../markdown_preview/src/markdown_renderer.rs | 28 ++- crates/picker/src/picker.rs | 39 ++-- crates/repl/src/notebook/notebook_ui.rs | 38 ++- .../src/project_index_debug_view.rs | 22 +- crates/zed/src/zed/component_preview.rs | 102 +++----- 15 files changed, 322 insertions(+), 403 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 6449643cac..f7c359fe99 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -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,10 +2536,21 @@ impl Render for AcpThreadView { v_flex().flex_1().map(|this| { if self.list_state.item_count() > 0 { this.child( - list(self.list_state.clone()) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), + 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(), ) .children(match thread_clone.read(cx).status() { ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 04a093c7d0..ed227f22e4 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -780,13 +780,7 @@ impl ActiveThread { cx.observe_global::(|_, 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) -> AnyElement { + fn render_message( + &mut self, + ix: usize, + window: &mut Window, + cx: &mut Context, + ) -> 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) }) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 717778bb98..0f2d431bc1 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -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); diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 3a9b568264..51d9f003f8 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -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| { - if event.visible_range.start < MESSAGE_LOADING_THRESHOLD { - this.load_more_messages(cx); - } - this.is_scrolled_to_bottom = !event.is_scrolled; - })); + 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, - ) -> 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) -> 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() diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index bae5dcfdb5..bb7c2ba1cd 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -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()) diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index c3e834b645..3a280ff667 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -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(), + ) } }) } diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index dd5487e042..6b376bb892 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -13,22 +13,8 @@ pub(crate) struct LoadedSourceList { impl LoadedSourceList { pub fn new(session: Entity, cx: &mut Context) -> 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(), + ) } } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index da3674c8e2..2149502f4a 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -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) -> 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) -> 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(), + ) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 709323ef58..39f38bdc69 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -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, style: StyleRefinement, sizing_behavior: ListSizingBehavior, } @@ -55,7 +62,6 @@ impl std::fmt::Debug for ListState { struct StateInner { last_layout_bounds: Option>, last_padding: Option>, - render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, 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( - 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, available_height: Pixels, padding: &Edges, + 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, padding: Edges, autoscroll: bool, + render_item: &mut RenderItemFn, window: &mut Window, cx: &mut App, ) -> Result { @@ -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,15 +956,16 @@ 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) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items(bounds, padding, false, window, cx) - .unwrap() - } - }; + 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, &mut self.render_item, window, cx) + .unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); @@ -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) -> 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) -> 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() } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 96e92de19c..a0c8819991 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -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, ) -> Entity { 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()).size_full())), - ) + .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() + .pl(render_cx.scaled_rems(1.0)) + .child(rendered_block), + ) + .child(indicator.absolute().left_0().top_0()), + ) + }) + .into_any() + }), + ) + .size_full(), + ) + })) } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 80bed8a6e8..37d2ca2110 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -26,7 +26,22 @@ use ui::{ }; use workspace::{OpenOptions, OpenVisible, Workspace}; -type CheckboxClickedCallback = Arc, &mut Window, &mut App)>>; +pub struct CheckboxClickedEvent { + pub checked: bool, + pub source_range: Range, +} + +impl CheckboxClickedEvent { + pub fn source_range(&self) -> Range { + self.source_range.clone() + } + + pub fn checked(&self) -> bool { + self.checked + } +} + +type CheckboxClickedCallback = Arc>; #[derive(Clone)] pub struct RenderContext { @@ -80,7 +95,7 @@ impl RenderContext { pub fn with_checkbox_clicked_callback( mut self, - callback: impl Fn(bool, Range, &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, + ); } } }) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 692bdd5bd7..34af5fed02 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -292,7 +292,7 @@ impl Picker { window: &mut Window, cx: &mut Context, ) -> 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 Picker { this } - fn create_element_container( - container: ContainerKind, - cx: &mut Context, - ) -> 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,11 +768,16 @@ impl Picker { .py_1() .track_scroll(scroll_handle.clone()) .into_any_element(), - ElementContainer::List(state) => list(state.clone()) - .with_sizing_behavior(sizing_behavior) - .flex_grow() - .py_2() - .into_any_element(), + 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() + .into_any_element(), } } diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 3e96cc4d11..2efa51e0cc 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -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)) } diff --git a/crates/semantic_index/src/project_index_debug_view.rs b/crates/semantic_index/src/project_index_debug_view.rs index 1b0d87fca0..8d6a49c45c 100644 --- a/crates/semantic_index/src/project_index_debug_view.rs +++ b/crates/semantic_index/src/project_index_debug_view.rs @@ -115,21 +115,9 @@ impl ProjectIndexDebugView { .collect::>(); 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 { diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 480505338b..db75b544f6 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -107,6 +107,7 @@ struct ComponentPreview { active_thread: Option>, reset_key: usize, component_list: ListState, + entries: Vec, component_map: HashMap, components: Vec, 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 { 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) { 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,10 +611,35 @@ impl ComponentPreview { .child(format!("No components matching '{}'.", self.filter_text)) .into_any_element() } else { - list(self.component_list.clone()) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .into_any_element() + 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() }, ) }