Start work on chat panel and non-uniform list

Max Brunsfeld and Nathan created

Co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/assistant2/src/assistant.rs                 |   4 
crates/assistant2/src/assistant_panel.rs           |   7 
crates/collab_ui2/src/chat_panel.rs                | 585 ++++++---------
crates/collab_ui2/src/chat_panel/message_editor.rs |  42 
crates/gpui2/src/elements/list.rs                  | 493 +++++++++++++
crates/gpui2/src/elements/mod.rs                   |   2 
crates/gpui2/src/elements/uniform_list.rs          |   2 
crates/gpui2/src/gpui2.rs                          |  20 
crates/rich_text2/src/rich_text.rs                 |   9 
9 files changed, 761 insertions(+), 403 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs 🔗

@@ -12,7 +12,7 @@ use chrono::{DateTime, Local};
 use collections::HashMap;
 use fs::Fs;
 use futures::StreamExt;
-use gpui::{actions, AppContext};
+use gpui::{actions, AppContext, SharedString};
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
@@ -47,7 +47,7 @@ struct MessageMetadata {
 enum MessageStatus {
     Pending,
     Done,
-    Error(Arc<str>),
+    Error(SharedString),
 }
 
 #[derive(Serialize, Deserialize)]

crates/assistant2/src/assistant_panel.rs 🔗

@@ -1628,8 +1628,9 @@ impl Conversation {
                                     metadata.status = MessageStatus::Done;
                                 }
                                 Err(error) => {
-                                    metadata.status =
-                                        MessageStatus::Error(error.to_string().trim().into());
+                                    metadata.status = MessageStatus::Error(SharedString::from(
+                                        error.to_string().trim().to_string(),
+                                    ));
                                 }
                             }
                             cx.notify();
@@ -2273,7 +2274,7 @@ impl ConversationEditor {
                                         Some(
                                             div()
                                                 .id("error")
-                                                .tooltip(move |cx| Tooltip::text(&error, cx))
+                                                .tooltip(move |cx| Tooltip::text(error.clone(), cx))
                                                 .child(IconElement::new(Icon::XCircle)),
                                         )
                                     } else {

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -1,6 +1,4 @@
-// use crate::{
-//     channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-// };
+// use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
 // use anyhow::Result;
 // use call::ActiveCall;
 // use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -9,13 +7,9 @@
 // use db::kvp::KEY_VALUE_STORE;
 // use editor::Editor;
 // use gpui::{
-//     actions,
-//     elements::*,
-//     platform::{CursorStyle, MouseButton},
-//     serde_json,
-//     views::{ItemType, Select, SelectStyle},
-//     AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
-//     ViewContext, ViewHandle, WeakViewHandle,
+//     actions, div, list, px, serde_json, AnyElement, AnyView, AppContext, AsyncAppContext, Div,
+//     Entity, EventEmitter, FocusableView, ListOffset, ListScrollHandle, Model, Orientation, Render,
+//     Subscription, Task, View, ViewContext, WeakView,
 // };
 // use language::LanguageRegistry;
 // use menu::Confirm;
@@ -23,10 +17,10 @@
 // use project::Fs;
 // use rich_text::RichText;
 // use serde::{Deserialize, Serialize};
-// use settings::SettingsStore;
+// use settings::{Settings, SettingsStore};
 // use std::sync::Arc;
-// use theme::{IconButton, Theme};
 // use time::{OffsetDateTime, UtcOffset};
+// use ui::{h_stack, v_stack, Avatar, Button, Label};
 // use util::{ResultExt, TryFutureExt};
 // use workspace::{
 //     dock::{DockPosition, Panel},
@@ -40,19 +34,18 @@
 
 // pub struct ChatPanel {
 //     client: Arc<Client>,
-//     channel_store: ModelHandle<ChannelStore>,
+//     channel_store: Model<ChannelStore>,
 //     languages: Arc<LanguageRegistry>,
-//     active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
-//     message_list: ListState<ChatPanel>,
-//     input_editor: ViewHandle<MessageEditor>,
-//     channel_select: ViewHandle<Select>,
+//     list_scroll: ListScrollHandle,
+//     active_chat: Option<(Model<ChannelChat>, Subscription)>,
+//     input_editor: View<MessageEditor>,
 //     local_timezone: UtcOffset,
 //     fs: Arc<dyn Fs>,
 //     width: Option<f32>,
 //     active: bool,
 //     pending_serialization: Task<Option<()>>,
 //     subscriptions: Vec<gpui::Subscription>,
-//     workspace: WeakViewHandle<Workspace>,
+//     workspace: WeakView<Workspace>,
 //     is_scrolled_to_bottom: bool,
 //     has_focus: bool,
 //     markdown_data: HashMap<ChannelMessageId, RichText>,
@@ -70,10 +63,7 @@
 //     Dismissed,
 // }
 
-// actions!(
-//     chat_panel,
-//     [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
-// );
+// actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall);
 
 // pub fn init(cx: &mut AppContext) {
 //     cx.add_action(ChatPanel::send);
@@ -83,7 +73,7 @@
 // }
 
 // impl ChatPanel {
-//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
 //         let fs = workspace.app_state().fs.clone();
 //         let client = workspace.app_state().client.clone();
 //         let channel_store = ChannelStore::global(cx);
@@ -93,53 +83,46 @@
 //             MessageEditor::new(
 //                 languages.clone(),
 //                 channel_store.clone(),
-//                 cx.add_view(|cx| {
-//                     Editor::auto_height(
-//                         4,
-//                         Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
-//                         cx,
-//                     )
-//                 }),
+//                 cx.add_view(|cx| Editor::auto_height(4, cx)),
 //                 cx,
 //             )
 //         });
 
 //         let workspace_handle = workspace.weak_handle();
 
-//         let channel_select = cx.add_view(|cx| {
-//             let channel_store = channel_store.clone();
-//             let workspace = workspace_handle.clone();
-//             Select::new(0, cx, {
-//                 move |ix, item_type, is_hovered, cx| {
-//                     Self::render_channel_name(
-//                         &channel_store,
-//                         ix,
-//                         item_type,
-//                         is_hovered,
-//                         workspace,
-//                         cx,
-//                     )
-//                 }
-//             })
-//             .with_style(move |cx| {
-//                 let style = &theme::current(cx).chat_panel.channel_select;
-//                 SelectStyle {
-//                     header: Default::default(),
-//                     menu: style.menu,
-//                 }
-//             })
-//         });
-
-//         let mut message_list =
-//             ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-//                 this.render_message(ix, cx)
-//             });
-//         message_list.set_scroll_handler(|visible_range, count, this, cx| {
-//             if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-//                 this.load_more_messages(&LoadMoreMessages, cx);
-//             }
-//             this.is_scrolled_to_bottom = visible_range.end == count;
-//         });
+//         // let channel_select = cx.add_view(|cx| {
+//         //     let channel_store = channel_store.clone();
+//         //     let workspace = workspace_handle.clone();
+//         //     Select::new(0, cx, {
+//         //         move |ix, item_type, is_hovered, cx| {
+//         //             Self::render_channel_name(
+//         //                 &channel_store,
+//         //                 ix,
+//         //                 item_type,
+//         //                 is_hovered,
+//         //                 workspace,
+//         //                 cx,
+//         //             )
+//         //         }
+//         //     })
+//         //     .with_style(move |cx| {
+//         //         let style = &cx.theme().chat_panel.channel_select;
+//         //         SelectStyle {
+//         //             header: Default::default(),
+//         //             menu: style.menu,
+//         //         }
+//         //     })
+//         // });
+
+//         // let mut message_list = ListState::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
+//         //     this.render_message(ix, cx)
+//         // });
+//         // 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.visible_range.end == event.count;
+//         // }));
 
 //         cx.add_view(|cx| {
 //             let mut this = Self {
@@ -147,11 +130,10 @@
 //                 client,
 //                 channel_store,
 //                 languages,
+//                 list_scroll: ListScrollHandle::new(),
 //                 active_chat: Default::default(),
 //                 pending_serialization: Task::ready(None),
-//                 message_list,
 //                 input_editor,
-//                 channel_select,
 //                 local_timezone: cx.platform().local_timezone(),
 //                 has_focus: false,
 //                 subscriptions: Vec::new(),
@@ -204,14 +186,11 @@
 //         self.is_scrolled_to_bottom
 //     }
 
-//     pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+//     pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
 //         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
 //     }
 
-//     pub fn load(
-//         workspace: WeakViewHandle<Workspace>,
-//         cx: AsyncAppContext,
-//     ) -> Task<Result<ViewHandle<Self>>> {
+//     pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
 //         cx.spawn(|mut cx| async move {
 //             let serialized_panel = if let Some(panel) = cx
 //                 .background()
@@ -261,7 +240,7 @@
 //         });
 //     }
 
-//     fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+//     fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
 //         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
 //             let channel_id = chat.read(cx).channel_id;
 //             {
@@ -288,7 +267,7 @@
 
 //     fn channel_did_change(
 //         &mut self,
-//         _: ModelHandle<ChannelChat>,
+//         _: Model<ChannelChat>,
 //         event: &ChannelChatEvent,
 //         cx: &mut ViewContext<Self>,
 //     ) {
@@ -326,30 +305,29 @@
 //         }
 //     }
 
-//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = theme::current(cx);
-//         Flex::column()
-//             .with_child(
-//                 ChildView::new(&self.channel_select, cx)
-//                     .contained()
-//                     .with_style(theme.chat_panel.channel_select.container),
-//             )
-//             .with_child(self.render_active_channel_messages(&theme))
-//             .with_child(self.render_input_box(&theme, cx))
+//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+//         v_stack()
+//             .child(Label::new(
+//                 self.active_chat.map_or(Default::default(), |c| {
+//                     c.0.read(cx).channel(cx)?.name.into()
+//                 }),
+//             ))
+//             .child(self.render_active_channel_messages(cx))
+//             .child(self.input_editor.to_any())
 //             .into_any()
 //     }
 
-//     fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
-//         let messages = if self.active_chat.is_some() {
-//             List::new(self.message_list.clone())
-//                 .contained()
-//                 .with_style(theme.chat_panel.list)
-//                 .into_any()
+//     fn render_active_channel_messages(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+//         if self.active_chat.is_some() {
+//             list(
+//                 Orientation::Bottom,
+//                 10.,
+//                 cx.listener(move |this, ix, cx| this.render_message(ix, cx)),
+//             )
+//             .into_any_element()
 //         } else {
-//             Empty::new().into_any()
-//         };
-
-//         messages.flex(1., true).into_any()
+//             div().into_any_element()
+//         }
 //     }
 
 //     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -388,21 +366,12 @@
 //             });
 
 //         let is_pending = message.is_pending();
-//         let theme = theme::current(cx);
 //         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
 //             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
 //         });
 
 //         let now = OffsetDateTime::now_utc();
 
-//         let style = if is_pending {
-//             &theme.chat_panel.pending_message
-//         } else if is_continuation {
-//             &theme.chat_panel.continuation_message
-//         } else {
-//             &theme.chat_panel.message
-//         };
-
 //         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
 //         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
 //             (message.id, belongs_to_user || is_admin)
@@ -412,89 +381,37 @@
 //             None
 //         };
 
-//         enum MessageBackgroundHighlight {}
-//         MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-//             let container = style.style_for(state);
-//             if is_continuation {
-//                 Flex::row()
-//                     .with_child(
-//                         text.element(
-//                             theme.editor.syntax.clone(),
-//                             theme.chat_panel.rich_text.clone(),
-//                             cx,
+//         if is_continuation {
+//             h_stack()
+//                 .child(text.element(cx))
+//                 .child(render_remove(message_id_to_remove, cx))
+//                 .mb_1()
+//                 .into_any()
+//         } else {
+//             v_stack()
+//                 .child(
+//                     h_stack()
+//                         .child(Avatar::data(message.sender.avatar.clone()))
+//                         .child(Label::new(message.sender.github_login.clone()))
+//                         .child(
+//                             Label::new(format_timestamp(
+//                                 message.timestamp,
+//                                 now,
+//                                 self.local_timezone,
+//                             ))
+//                             .flex(1., true),
 //                         )
-//                         .flex(1., true),
-//                     )
-//                     .with_child(render_remove(message_id_to_remove, cx, &theme))
-//                     .contained()
-//                     .with_style(*container)
-//                     .with_margin_bottom(if is_last {
-//                         theme.chat_panel.last_message_bottom_spacing
-//                     } else {
-//                         0.
-//                     })
-//                     .into_any()
-//             } else {
-//                 Flex::column()
-//                     .with_child(
-//                         Flex::row()
-//                             .with_child(
-//                                 Flex::row()
-//                                     .with_child(render_avatar(
-//                                         message.sender.avatar.clone(),
-//                                         &theme.chat_panel.avatar,
-//                                         theme.chat_panel.avatar_container,
-//                                     ))
-//                                     .with_child(
-//                                         Label::new(
-//                                             message.sender.github_login.clone(),
-//                                             theme.chat_panel.message_sender.text.clone(),
-//                                         )
-//                                         .contained()
-//                                         .with_style(theme.chat_panel.message_sender.container),
-//                                     )
-//                                     .with_child(
-//                                         Label::new(
-//                                             format_timestamp(
-//                                                 message.timestamp,
-//                                                 now,
-//                                                 self.local_timezone,
-//                                             ),
-//                                             theme.chat_panel.message_timestamp.text.clone(),
-//                                         )
-//                                         .contained()
-//                                         .with_style(theme.chat_panel.message_timestamp.container),
-//                                     )
-//                                     .align_children_center()
-//                                     .flex(1., true),
-//                             )
-//                             .with_child(render_remove(message_id_to_remove, cx, &theme))
-//                             .align_children_center(),
-//                     )
-//                     .with_child(
-//                         Flex::row()
-//                             .with_child(
-//                                 text.element(
-//                                     theme.editor.syntax.clone(),
-//                                     theme.chat_panel.rich_text.clone(),
-//                                     cx,
-//                                 )
-//                                 .flex(1., true),
-//                             )
-//                             // Add a spacer to make everything line up
-//                             .with_child(render_remove(None, cx, &theme)),
-//                     )
-//                     .contained()
-//                     .with_style(*container)
-//                     .with_margin_bottom(if is_last {
-//                         theme.chat_panel.last_message_bottom_spacing
-//                     } else {
-//                         0.
-//                     })
-//                     .into_any()
-//             }
-//         })
-//         .into_any()
+//                         .child(render_remove(message_id_to_remove, cx))
+//                         .align_children_center(),
+//                 )
+//                 .child(
+//                     h_stack()
+//                         .child(text.element(cx))
+//                         .child(render_remove(None, cx)),
+//                 )
+//                 .mb_1()
+//                 .into_any()
+//         }
 //     }
 
 //     fn render_markdown_with_mentions(
@@ -514,127 +431,106 @@
 //         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
 //     }
 
-//     fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
-//         ChildView::new(&self.input_editor, cx)
-//             .contained()
-//             .with_style(theme.chat_panel.input_editor.container)
-//             .into_any()
-//     }
-
-//     fn render_channel_name(
-//         channel_store: &ModelHandle<ChannelStore>,
-//         ix: usize,
-//         item_type: ItemType,
-//         is_hovered: bool,
-//         workspace: WeakViewHandle<Workspace>,
-//         cx: &mut ViewContext<Select>,
-//     ) -> AnyElement<Select> {
-//         let theme = theme::current(cx);
-//         let tooltip_style = &theme.tooltip;
-//         let theme = &theme.chat_panel;
-//         let style = match (&item_type, is_hovered) {
-//             (ItemType::Header, _) => &theme.channel_select.header,
-//             (ItemType::Selected, _) => &theme.channel_select.active_item,
-//             (ItemType::Unselected, false) => &theme.channel_select.item,
-//             (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-//         };
-
-//         let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-//         let channel_id = channel.id;
-
-//         let mut row = Flex::row()
-//             .with_child(
-//                 Label::new("#".to_string(), style.hash.text.clone())
-//                     .contained()
-//                     .with_style(style.hash.container),
-//             )
-//             .with_child(Label::new(channel.name.clone(), style.name.clone()));
+//     // fn render_channel_name(
+//     //     channel_store: &Model<ChannelStore>,
+//     //     ix: usize,
+//     //     item_type: ItemType,
+//     //     is_hovered: bool,
+//     //     workspace: WeakView<Workspace>,
+//     //     cx: &mut ViewContext<Select>,
+//     // ) -> AnyElement<Select> {
+//     //     let theme = theme::current(cx);
+//     //     let tooltip_style = &theme.tooltip;
+//     //     let theme = &theme.chat_panel;
+//     //     let style = match (&item_type, is_hovered) {
+//     //         (ItemType::Header, _) => &theme.channel_select.header,
+//     //         (ItemType::Selected, _) => &theme.channel_select.active_item,
+//     //         (ItemType::Unselected, false) => &theme.channel_select.item,
+//     //         (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
+//     //     };
+
+//     //     let channel = &channel_store.read(cx).channel_at(ix).unwrap();
+//     //     let channel_id = channel.id;
+
+//     //     let mut row = Flex::row()
+//     //         .with_child(
+//     //             Label::new("#".to_string(), style.hash.text.clone())
+//     //                 .contained()
+//     //                 .with_style(style.hash.container),
+//     //         )
+//     //         .with_child(Label::new(channel.name.clone(), style.name.clone()));
+
+//     //     if matches!(item_type, ItemType::Header) {
+//     //         row.add_children([
+//     //             MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
+//     //                 render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
+//     //             })
+//     //             .on_click(MouseButton::Left, move |_, _, cx| {
+//     //                 if let Some(workspace) = workspace.upgrade(cx) {
+//     //                     ChannelView::open(channel_id, workspace, cx).detach();
+//     //                 }
+//     //             })
+//     //             .with_tooltip::<OpenChannelNotes>(
+//     //                 channel_id as usize,
+//     //                 "Open Notes",
+//     //                 Some(Box::new(OpenChannelNotes)),
+//     //                 tooltip_style.clone(),
+//     //                 cx,
+//     //             )
+//     //             .flex_float(),
+//     //             MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
+//     //                 render_icon_button(
+//     //                     theme.icon_button.style_for(mouse_state),
+//     //                     "icons/speaker-loud.svg",
+//     //                 )
+//     //             })
+//     //             .on_click(MouseButton::Left, move |_, _, cx| {
+//     //                 ActiveCall::global(cx)
+//     //                     .update(cx, |call, cx| call.join_channel(channel_id, cx))
+//     //                     .detach_and_log_err(cx);
+//     //             })
+//     //             .with_tooltip::<ActiveCall>(
+//     //                 channel_id as usize,
+//     //                 "Join Call",
+//     //                 Some(Box::new(JoinCall)),
+//     //                 tooltip_style.clone(),
+//     //                 cx,
+//     //             )
+//     //             .flex_float(),
+//     //         ]);
+//     //     }
+
+//     //     row.align_children_center()
+//     //         .contained()
+//     //         .with_style(style.container)
+//     //         .into_any()
+//     // }
+
+//     fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+//         enum SignInPromptLabel {}
 
-//         if matches!(item_type, ItemType::Header) {
-//             row.add_children([
-//                 MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-//                     render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-//                 })
-//                 .on_click(MouseButton::Left, move |_, _, cx| {
-//                     if let Some(workspace) = workspace.upgrade(cx) {
-//                         ChannelView::open(channel_id, workspace, cx).detach();
+//         Button::new("sign-in", "Sign in to use chat")
+//             .on_click(move |_, this, cx| {
+//                 let client = this.client.clone();
+//                 cx.spawn(|this, mut cx| async move {
+//                     if client
+//                         .authenticate_and_connect(true, &cx)
+//                         .log_err()
+//                         .await
+//                         .is_some()
+//                     {
+//                         this.update(&mut cx, |this, cx| {
+//                             if cx.handle().is_focused(cx) {
+//                                 cx.focus(&this.input_editor);
+//                             }
+//                         })
+//                         .ok();
 //                     }
 //                 })
-//                 .with_tooltip::<OpenChannelNotes>(
-//                     channel_id as usize,
-//                     "Open Notes",
-//                     Some(Box::new(OpenChannelNotes)),
-//                     tooltip_style.clone(),
-//                     cx,
-//                 )
-//                 .flex_float(),
-//                 MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-//                     render_icon_button(
-//                         theme.icon_button.style_for(mouse_state),
-//                         "icons/speaker-loud.svg",
-//                     )
-//                 })
-//                 .on_click(MouseButton::Left, move |_, _, cx| {
-//                     ActiveCall::global(cx)
-//                         .update(cx, |call, cx| call.join_channel(channel_id, cx))
-//                         .detach_and_log_err(cx);
-//                 })
-//                 .with_tooltip::<ActiveCall>(
-//                     channel_id as usize,
-//                     "Join Call",
-//                     Some(Box::new(JoinCall)),
-//                     tooltip_style.clone(),
-//                     cx,
-//                 )
-//                 .flex_float(),
-//             ]);
-//         }
-
-//         row.align_children_center()
-//             .contained()
-//             .with_style(style.container)
-//             .into_any()
-//     }
-
-//     fn render_sign_in_prompt(
-//         &self,
-//         theme: &Arc<Theme>,
-//         cx: &mut ViewContext<Self>,
-//     ) -> AnyElement<Self> {
-//         enum SignInPromptLabel {}
-
-//         MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-//             Label::new(
-//                 "Sign in to use chat".to_string(),
-//                 theme
-//                     .chat_panel
-//                     .sign_in_prompt
-//                     .style_for(mouse_state)
-//                     .clone(),
-//             )
-//         })
-//         .with_cursor_style(CursorStyle::PointingHand)
-//         .on_click(MouseButton::Left, move |_, this, cx| {
-//             let client = this.client.clone();
-//             cx.spawn(|this, mut cx| async move {
-//                 if client
-//                     .authenticate_and_connect(true, &cx)
-//                     .log_err()
-//                     .await
-//                     .is_some()
-//                 {
-//                     this.update(&mut cx, |this, cx| {
-//                         if cx.handle().is_focused(cx) {
-//                             cx.focus(&this.input_editor);
-//                         }
-//                     })
-//                     .ok();
-//                 }
+//                 .detach();
 //             })
-//             .detach();
-//         })
-//         .aligned()
-//         .into_any()
+//             .aligned()
+//             .into_any()
 //     }
 
 //     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@@ -700,9 +596,9 @@
 //                 {
 //                     this.update(&mut cx, |this, cx| {
 //                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-//                             this.message_list.scroll_to(ListOffset {
+//                             this.list_scroll.scroll_to(ListOffset {
 //                                 item_ix,
-//                                 offset_in_item: 0.,
+//                                 offset_in_item: px(0.0),
 //                             });
 //                             cx.notify();
 //                         }
@@ -733,11 +629,7 @@
 //     }
 // }
 
-// fn render_remove(
-//     message_id_to_remove: Option<u64>,
-//     cx: &mut ViewContext<'_, '_, ChatPanel>,
-//     theme: &Arc<Theme>,
-// ) -> AnyElement<ChatPanel> {
+// fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
 //     enum DeleteMessage {}
 
 //     message_id_to_remove
@@ -773,49 +665,31 @@
 //         })
 // }
 
-// impl Entity for ChatPanel {
-//     type Event = Event;
-// }
+// impl EventEmitter<Event> for ChatPanel {}
 
-// impl View for ChatPanel {
-//     fn ui_name() -> &'static str {
-//         "ChatPanel"
-//     }
+// impl Render for ChatPanel {
+//     type Element = Div;
 
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = theme::current(cx);
-//         let element = if self.client.user_id().is_some() {
-//             self.render_channel(cx)
-//         } else {
-//             self.render_sign_in_prompt(&theme, cx)
-//         };
-//         element
-//             .contained()
-//             .with_style(theme.chat_panel.container)
-//             .constrained()
-//             .with_min_width(150.)
-//             .into_any()
-//     }
-
-//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-//         self.has_focus = true;
-//         if matches!(
-//             *self.client.status().borrow(),
-//             client::Status::Connected { .. }
-//         ) {
-//             let editor = self.input_editor.read(cx).editor.clone();
-//             cx.focus(&editor);
-//         }
+//     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+//         div()
+//             .child(if self.client.user_id().is_some() {
+//                 self.render_channel(cx)
+//             } else {
+//                 self.render_sign_in_prompt(cx)
+//             })
+//             .min_w(px(150.))
 //     }
+// }
 
-//     fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-//         self.has_focus = false;
+// impl FocusableView for ChatPanel {
+//     fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+//         self.input_editor.read(cx).focus_handle(cx)
 //     }
 // }
 
 // impl Panel for ChatPanel {
 //     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-//         settings::get::<ChatPanelSettings>(cx).dock
+//         ChatPanelSettings::get_global(cx).dock
 //     }
 
 //     fn position_is_valid(&self, position: DockPosition) -> bool {
@@ -830,7 +704,7 @@
 
 //     fn size(&self, cx: &gpui::WindowContext) -> f32 {
 //         self.width
-//             .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+//             .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
 //     }
 
 //     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
@@ -849,29 +723,16 @@
 //         }
 //     }
 
-//     fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-//         (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
-//             .then(|| "icons/conversations.svg")
-//     }
-
-//     fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-//         ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
-//     }
-
-//     fn should_change_position_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::DockPositionChanged)
-//     }
-
-//     fn should_close_on_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Dismissed)
+//     fn persistent_name() -> &'static str {
+//         todo!()
 //     }
 
-//     fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-//         self.has_focus
+//     fn icon(&self, cx: &ui::prelude::WindowContext) -> Option<ui::Icon> {
+//         Some(ui::Icon::MessageBubbles)
 //     }
 
-//     fn is_focus_event(event: &Self::Event) -> bool {
-//         matches!(event, Event::Focus)
+//     fn toggle_action(&self) -> Box<dyn gpui::Action> {
+//         todo!()
 //     }
 // }
 

crates/collab_ui2/src/chat_panel/message_editor.rs 🔗

@@ -2,14 +2,12 @@ use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
 use client::UserId;
 use collections::HashMap;
 use editor::{AnchorRangeExt, Editor};
-use gpui::{
-    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
-};
+use gpui::{AnyView, AsyncAppContext, Model, Render, Task, View, ViewContext, WeakView};
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
 use std::{sync::Arc, time::Duration};
+use workspace::item::ItemHandle;
 
 const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 
@@ -19,8 +17,8 @@ lazy_static! {
 }
 
 pub struct MessageEditor {
-    pub editor: ViewHandle<Editor>,
-    channel_store: ModelHandle<ChannelStore>,
+    pub editor: View<Editor>,
+    channel_store: Model<ChannelStore>,
     users: HashMap<String, UserId>,
     mentions: Vec<UserId>,
     mentions_task: Option<Task<()>>,
@@ -30,8 +28,8 @@ pub struct MessageEditor {
 impl MessageEditor {
     pub fn new(
         language_registry: Arc<LanguageRegistry>,
-        channel_store: ModelHandle<ChannelStore>,
-        editor: ViewHandle<Editor>,
+        channel_store: Model<ChannelStore>,
+        editor: View<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         editor.update(cx, |editor, cx| {
@@ -132,7 +130,7 @@ impl MessageEditor {
 
     fn on_buffer_event(
         &mut self,
-        buffer: ModelHandle<Buffer>,
+        buffer: Model<Buffer>,
         event: &language::Event,
         cx: &mut ViewContext<Self>,
     ) {
@@ -146,7 +144,7 @@ impl MessageEditor {
     }
 
     async fn find_mentions(
-        this: WeakViewHandle<MessageEditor>,
+        this: WeakView<MessageEditor>,
         buffer: BufferSnapshot,
         mut cx: AsyncAppContext,
     ) {
@@ -180,11 +178,7 @@ impl MessageEditor {
                 }
 
                 editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(
-                    anchor_ranges,
-                    theme::current(cx).chat_panel.rich_text.mention_highlight,
-                    cx,
-                )
+                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
             });
 
             this.mentions = mentioned_user_ids;
@@ -194,19 +188,11 @@ impl MessageEditor {
     }
 }
 
-impl Entity for MessageEditor {
-    type Event = ();
-}
-
-impl View for MessageEditor {
-    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
-        ChildView::new(&self.editor, cx).into_any()
-    }
+impl Render for MessageEditor {
+    type Element = AnyView;
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.editor);
-        }
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        self.editor.to_any()
     }
 }
 
@@ -297,7 +283,7 @@ mod tests {
             MessageEditor::new(
                 language_registry,
                 ChannelStore::global(cx),
-                cx.add_view(|cx| Editor::auto_height(4, None, cx)),
+                cx.add_view(|cx| Editor::auto_height(4, cx)),
                 cx,
             )
         });

crates/gpui2/src/elements/list.rs 🔗

@@ -0,0 +1,493 @@
+use crate::{
+    px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
+    Point, ScrollWheelEvent, Size, Style, StyleRefinement, ViewContext, WindowContext,
+};
+use collections::VecDeque;
+use std::{cell::RefCell, ops::Range, rc::Rc};
+use sum_tree::{Bias, SumTree};
+
+pub fn list(state: ListState) -> List {
+    List {
+        state,
+        style: StyleRefinement::default(),
+    }
+}
+
+pub struct List {
+    state: ListState,
+    style: StyleRefinement,
+}
+
+#[derive(Clone)]
+pub struct ListState(Rc<RefCell<StateInner>>);
+
+struct StateInner {
+    last_layout_width: Option<Pixels>,
+    render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
+    items: SumTree<ListItem>,
+    logical_scroll_top: Option<ListOffset>,
+    orientation: Orientation,
+    overdraw: Pixels,
+    #[allow(clippy::type_complexity)]
+    scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum Orientation {
+    Top,
+    Bottom,
+}
+
+pub struct ListScrollEvent {
+    pub visible_range: Range<usize>,
+    pub count: usize,
+}
+
+#[derive(Clone)]
+enum ListItem {
+    Unrendered,
+    Rendered { height: Pixels },
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+struct ListItemSummary {
+    count: usize,
+    rendered_count: usize,
+    unrendered_count: usize,
+    height: Pixels,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct RenderedCount(usize);
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct UnrenderedCount(usize);
+
+#[derive(Clone, Debug, Default)]
+struct Height(Pixels);
+
+impl ListState {
+    pub fn new<F, V>(
+        element_count: usize,
+        orientation: Orientation,
+        overdraw: Pixels,
+        cx: &mut ViewContext<V>,
+        mut render_item: F,
+    ) -> Self
+    where
+        F: 'static + FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement,
+        V: 'static,
+    {
+        let mut items = SumTree::new();
+        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
+        let view = cx.view().clone();
+        Self(Rc::new(RefCell::new(StateInner {
+            last_layout_width: None,
+            render_item: Box::new(move |ix, cx| {
+                view.update(cx, |view, cx| render_item(view, ix, cx))
+            }),
+            items,
+            logical_scroll_top: None,
+            orientation,
+            overdraw,
+            scroll_handler: None,
+        })))
+    }
+
+    pub fn reset(&self, element_count: usize) {
+        let state = &mut *self.0.borrow_mut();
+        state.logical_scroll_top = None;
+        state.items = SumTree::new();
+        state
+            .items
+            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
+    }
+
+    pub fn item_count(&self) -> usize {
+        self.0.borrow().items.summary().count
+    }
+
+    pub fn splice(&self, old_range: Range<usize>, count: usize) {
+        let state = &mut *self.0.borrow_mut();
+
+        if let Some(ListOffset {
+            item_ix,
+            offset_in_item,
+        }) = state.logical_scroll_top.as_mut()
+        {
+            if old_range.contains(item_ix) {
+                *item_ix = old_range.start;
+                *offset_in_item = px(0.);
+            } else if old_range.end <= *item_ix {
+                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
+            }
+        }
+
+        let mut old_heights = state.items.cursor::<Count>();
+        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
+        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
+
+        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
+        new_heights.append(old_heights.suffix(&()), &());
+        drop(old_heights);
+        state.items = new_heights;
+    }
+
+    pub fn set_scroll_handler(
+        &mut self,
+        handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
+    ) {
+        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
+    }
+
+    pub fn logical_scroll_top(&self) -> ListOffset {
+        self.0.borrow().logical_scroll_top()
+    }
+
+    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
+        let state = &mut *self.0.borrow_mut();
+        let item_count = state.items.summary().count;
+        if scroll_top.item_ix >= item_count {
+            scroll_top.item_ix = item_count;
+            scroll_top.offset_in_item = px(0.);
+        }
+        state.logical_scroll_top = Some(scroll_top);
+    }
+}
+
+impl StateInner {
+    fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
+        let mut cursor = self.items.cursor::<ListItemSummary>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        let start_y = cursor.start().height + scroll_top.offset_in_item;
+        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
+        scroll_top.item_ix..cursor.start().count + 1
+    }
+
+    fn scroll(
+        &mut self,
+        scroll_top: &ListOffset,
+        height: Pixels,
+        delta: Point<Pixels>,
+        cx: &mut WindowContext,
+    ) {
+        let scroll_max = (self.items.summary().height - height).max(px(0.));
+        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
+            .max(px(0.))
+            .min(scroll_max);
+
+        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
+            self.logical_scroll_top = None;
+        } else {
+            let mut cursor = self.items.cursor::<ListItemSummary>();
+            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
+            let item_ix = cursor.start().count;
+            let offset_in_item = new_scroll_top - cursor.start().height;
+            self.logical_scroll_top = Some(ListOffset {
+                item_ix,
+                offset_in_item,
+            });
+        }
+
+        if self.scroll_handler.is_some() {
+            let visible_range = self.visible_range(height, scroll_top);
+            self.scroll_handler.as_mut().unwrap()(
+                &ListScrollEvent {
+                    visible_range,
+                    count: self.items.summary().count,
+                },
+                cx,
+            );
+        }
+
+        cx.notify();
+    }
+
+    fn logical_scroll_top(&self) -> ListOffset {
+        self.logical_scroll_top
+            .unwrap_or_else(|| match self.orientation {
+                Orientation::Top => ListOffset {
+                    item_ix: 0,
+                    offset_in_item: px(0.),
+                },
+                Orientation::Bottom => ListOffset {
+                    item_ix: self.items.summary().count,
+                    offset_in_item: px(0.),
+                },
+            })
+    }
+
+    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
+        let mut cursor = self.items.cursor::<ListItemSummary>();
+        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
+        cursor.start().height + logical_scroll_top.offset_in_item
+    }
+}
+
+impl std::fmt::Debug for ListItem {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Unrendered => write!(f, "Unrendered"),
+            Self::Rendered { height, .. } => {
+                f.debug_struct("Rendered").field("height", height).finish()
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct ListOffset {
+    pub item_ix: usize,
+    pub offset_in_item: Pixels,
+}
+
+impl Element for List {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _state: Option<Self::State>,
+        cx: &mut crate::WindowContext,
+    ) -> (crate::LayoutId, Self::State) {
+        let style = Style::from(self.style.clone());
+        let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
+            cx.request_layout(&style, None)
+        });
+        (layout_id, ())
+    }
+
+    fn paint(
+        self,
+        bounds: crate::Bounds<crate::Pixels>,
+        _state: &mut Self::State,
+        cx: &mut crate::WindowContext,
+    ) {
+        let state = &mut *self.state.0.borrow_mut();
+
+        // If the width of the list has changed, invalidate all cached item heights
+        if state.last_layout_width != Some(bounds.size.width) {
+            state.items = SumTree::from_iter(
+                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
+                &(),
+            )
+        }
+
+        let old_items = state.items.clone();
+        let mut measured_items = VecDeque::new();
+        let mut item_elements = VecDeque::new();
+        let mut rendered_height = px(0.);
+        let mut scroll_top = state.logical_scroll_top();
+
+        let available_item_space = Size {
+            width: AvailableSpace::Definite(bounds.size.width),
+            height: AvailableSpace::MinContent,
+        };
+
+        // Render items after the scroll top, including those in the trailing overdraw
+        let mut cursor = old_items.cursor::<Count>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        for (ix, item) in cursor.by_ref().enumerate() {
+            let visible_height = rendered_height - scroll_top.offset_in_item;
+            if visible_height >= bounds.size.height + state.overdraw {
+                break;
+            }
+
+            // Use the previously cached height if available
+            let mut height = if let ListItem::Rendered { height } = item {
+                Some(*height)
+            } else {
+                None
+            };
+
+            // If we're within the visible area or the height wasn't cached, render and measure the item's element
+            if visible_height < bounds.size.height || height.is_none() {
+                let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
+                let element_size = element.measure(available_item_space, cx);
+                height = Some(element_size.height);
+                if visible_height < bounds.size.height {
+                    item_elements.push_back(element);
+                }
+            }
+
+            let height = height.unwrap();
+            rendered_height += height;
+            measured_items.push_back(ListItem::Rendered { height });
+        }
+
+        // Prepare to start walking upward from the item at the scroll top.
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+
+        // If the rendered items do not fill the visible region, then adjust
+        // the scroll top upward.
+        if rendered_height - scroll_top.offset_in_item < bounds.size.height {
+            while rendered_height < bounds.size.height {
+                cursor.prev(&());
+                if cursor.item().is_some() {
+                    let mut element = (state.render_item)(cursor.start().0, cx);
+                    let element_size = element.measure(available_item_space, cx);
+
+                    rendered_height += element_size.height;
+                    measured_items.push_front(ListItem::Rendered {
+                        height: element_size.height,
+                    });
+                    item_elements.push_front(element)
+                } else {
+                    break;
+                }
+            }
+
+            scroll_top = ListOffset {
+                item_ix: cursor.start().0,
+                offset_in_item: rendered_height - bounds.size.height,
+            };
+
+            match state.orientation {
+                Orientation::Top => {
+                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
+                    state.logical_scroll_top = Some(scroll_top);
+                }
+                Orientation::Bottom => {
+                    scroll_top = ListOffset {
+                        item_ix: cursor.start().0,
+                        offset_in_item: rendered_height - bounds.size.height,
+                    };
+                    state.logical_scroll_top = None;
+                }
+            };
+        }
+
+        // Measure items in the leading overdraw
+        let mut leading_overdraw = scroll_top.offset_in_item;
+        while leading_overdraw < state.overdraw {
+            cursor.prev(&());
+            if let Some(item) = cursor.item() {
+                let height = if let ListItem::Rendered { height } = item {
+                    *height
+                } else {
+                    let mut element = (state.render_item)(cursor.start().0, cx);
+                    element.measure(available_item_space, cx).height
+                };
+
+                leading_overdraw += height;
+                measured_items.push_front(ListItem::Rendered { height });
+            } else {
+                break;
+            }
+        }
+
+        let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
+        let mut cursor = old_items.cursor::<Count>();
+        let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
+        new_items.extend(measured_items, &());
+        cursor.seek(&Count(measured_range.end), Bias::Right, &());
+        new_items.append(cursor.suffix(&()), &());
+
+        // Paint the visible items
+        let mut item_origin = bounds.origin;
+        item_origin.y -= scroll_top.offset_in_item;
+        for mut item_element in item_elements {
+            let item_height = item_element.measure(available_item_space, cx).height;
+            item_element.draw(item_origin, available_item_space, cx);
+            item_origin.y += item_height;
+        }
+
+        state.items = new_items;
+        state.last_layout_width = Some(bounds.size.width);
+
+        let list_state = self.state.clone();
+        let height = bounds.size.height;
+        cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble {
+                list_state.0.borrow_mut().scroll(
+                    &scroll_top,
+                    height,
+                    event.delta.pixel_delta(px(20.)),
+                    cx,
+                )
+            }
+        });
+    }
+}
+
+impl IntoElement for List {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<crate::ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl sum_tree::Item for ListItem {
+    type Summary = ListItemSummary;
+
+    fn summary(&self) -> Self::Summary {
+        match self {
+            ListItem::Unrendered => ListItemSummary {
+                count: 1,
+                rendered_count: 0,
+                unrendered_count: 1,
+                height: px(0.),
+            },
+            ListItem::Rendered { height } => ListItemSummary {
+                count: 1,
+                rendered_count: 1,
+                unrendered_count: 0,
+                height: *height,
+            },
+        }
+    }
+}
+
+impl sum_tree::Summary for ListItemSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.count += summary.count;
+        self.rendered_count += summary.rendered_count;
+        self.unrendered_count += summary.unrendered_count;
+        self.height += summary.height;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.rendered_count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.unrendered_count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
+    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
+        self.0 += summary.height;
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
+    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+        self.0.partial_cmp(&other.count).unwrap()
+    }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
+    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
+        self.0.partial_cmp(&other.height).unwrap()
+    }
+}

crates/gpui2/src/elements/mod.rs 🔗

@@ -1,6 +1,7 @@
 mod canvas;
 mod div;
 mod img;
+mod list;
 mod overlay;
 mod svg;
 mod text;
@@ -9,6 +10,7 @@ mod uniform_list;
 pub use canvas::*;
 pub use div::*;
 pub use img::*;
+pub use list::*;
 pub use overlay::*;
 pub use svg::*;
 pub use text::*;

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

@@ -131,7 +131,7 @@ impl Element for UniformList {
                                         }
                                     });
                             let height = match available_space.height {
-                                AvailableSpace::Definite(x) => desired_height.min(x),
+                                AvailableSpace::Definite(height) => desired_height.min(height),
                                 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
                                     desired_height
                                 }

crates/gpui2/src/gpui2.rs 🔗

@@ -72,6 +72,7 @@ pub use view::*;
 pub use window::*;
 
 use derive_more::{Deref, DerefMut};
+use serde::{Deserialize, Serialize};
 use std::{
     any::{Any, TypeId},
     borrow::{Borrow, BorrowMut},
@@ -248,3 +249,22 @@ impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
         Self(value.into())
     }
 }
+
+impl Serialize for SharedString {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_ref())
+    }
+}
+
+impl<'de> Deserialize<'de> for SharedString {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(SharedString::from(s))
+    }
+}

crates/rich_text2/src/rich_text.rs 🔗

@@ -2,7 +2,7 @@ use std::{ops::Range, sync::Arc};
 
 use anyhow::bail;
 use futures::FutureExt;
-use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle};
+use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle, WindowContext};
 use language::{HighlightId, Language, LanguageRegistry};
 use util::RangeExt;
 
@@ -56,12 +56,7 @@ pub struct Mention {
 }
 
 impl RichText {
-    pub fn element(
-        &self,
-        // syntax: Arc<SyntaxTheme>,
-        //  style: RichTextStyle,
-        // cx: &mut ViewContext<V>,
-    ) -> AnyElement {
+    pub fn element(&self, cx: &mut WindowContext) -> AnyElement {
         todo!();
 
         // let mut region_id = 0;