mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
Start work on chat panel and non-uniform list
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
4353bdb9d5
commit
6955579f19
@ -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)]
|
||||
|
@ -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 {
|
||||
|
@ -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 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::<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 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: &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,
|
||||
// // };
|
||||
|
||||
// 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 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()));
|
||||
|
||||
// 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(),
|
||||
// // ]);
|
||||
// // }
|
||||
|
||||
// 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()
|
||||
// // }
|
||||
|
||||
// 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> {
|
||||
// fn render_sign_in_prompt(&self, 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();
|
||||
// }
|
||||
// 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();
|
||||
// }
|
||||
// })
|
||||
// .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 Render for ChatPanel {
|
||||
// type Element = Div;
|
||||
|
||||
// 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.))
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl View for ChatPanel {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChatPanel"
|
||||
// }
|
||||
|
||||
// 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 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 persistent_name() -> &'static str {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||
// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
|
||||
// fn icon(&self, cx: &ui::prelude::WindowContext) -> Option<ui::Icon> {
|
||||
// Some(ui::Icon::MessageBubbles)
|
||||
// }
|
||||
|
||||
// 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 has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// 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 Render for MessageEditor {
|
||||
type Element = AnyView;
|
||||
|
||||
impl View for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
});
|
||||
|
493
crates/gpui2/src/elements/list.rs
Normal file
493
crates/gpui2/src/elements/list.rs
Normal file
@ -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()
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user