@@ -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!()
// }
// }
@@ -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,
)
});
@@ -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()
+ }
+}