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:
parent
d0de81b0b4
commit
53175263a1
15 changed files with 322 additions and 403 deletions
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue