Merge branch 'main' into tabs-n-splits
This commit is contained in:
commit
9da0b78ead
48 changed files with 10109 additions and 625 deletions
42
Cargo.lock
generated
42
Cargo.lock
generated
|
@ -1829,6 +1829,47 @@ dependencies = [
|
||||||
"zed-actions",
|
"zed-actions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "collab_ui2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"call2",
|
||||||
|
"channel2",
|
||||||
|
"client2",
|
||||||
|
"clock",
|
||||||
|
"collections",
|
||||||
|
"db2",
|
||||||
|
"editor2",
|
||||||
|
"feature_flags2",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui2",
|
||||||
|
"language2",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"menu2",
|
||||||
|
"notifications2",
|
||||||
|
"picker2",
|
||||||
|
"postage",
|
||||||
|
"pretty_assertions",
|
||||||
|
"project2",
|
||||||
|
"rich_text2",
|
||||||
|
"rpc2",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"settings2",
|
||||||
|
"smallvec",
|
||||||
|
"theme2",
|
||||||
|
"time",
|
||||||
|
"tree-sitter-markdown",
|
||||||
|
"ui2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
"zed_actions2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -11441,6 +11482,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cli",
|
"cli",
|
||||||
"client2",
|
"client2",
|
||||||
|
"collab_ui2",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette2",
|
"command_palette2",
|
||||||
"copilot2",
|
"copilot2",
|
||||||
|
|
|
@ -18,6 +18,7 @@ members = [
|
||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab2",
|
"crates/collab2",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
|
"crates/collab_ui2",
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
"crates/command_palette",
|
"crates/command_palette",
|
||||||
"crates/command_palette2",
|
"crates/command_palette2",
|
||||||
|
|
81
crates/collab_ui2/Cargo.toml
Normal file
81
crates/collab_ui2/Cargo.toml
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
[package]
|
||||||
|
name = "collab_ui2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/collab_ui.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = [
|
||||||
|
"call/test-support",
|
||||||
|
"client/test-support",
|
||||||
|
"collections/test-support",
|
||||||
|
"editor/test-support",
|
||||||
|
"gpui/test-support",
|
||||||
|
"project/test-support",
|
||||||
|
"settings/test-support",
|
||||||
|
"util/test-support",
|
||||||
|
"workspace/test-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# auto_update = { path = "../auto_update" }
|
||||||
|
db = { package = "db2", path = "../db2" }
|
||||||
|
call = { package = "call2", path = "../call2" }
|
||||||
|
client = { package = "client2", path = "../client2" }
|
||||||
|
channel = { package = "channel2", path = "../channel2" }
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
# context_menu = { path = "../context_menu" }
|
||||||
|
# drag_and_drop = { path = "../drag_and_drop" }
|
||||||
|
editor = { package="editor2", path = "../editor2" }
|
||||||
|
#feedback = { path = "../feedback" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
language = { package = "language2", path = "../language2" }
|
||||||
|
menu = { package = "menu2", path = "../menu2" }
|
||||||
|
notifications = { package = "notifications2", path = "../notifications2" }
|
||||||
|
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||||
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
|
project = { package = "project2", path = "../project2" }
|
||||||
|
# recent_projects = { path = "../recent_projects" }
|
||||||
|
rpc = { package ="rpc2", path = "../rpc2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
# theme_selector = { path = "../theme_selector" }
|
||||||
|
# vcs_menu = { path = "../vcs_menu" }
|
||||||
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
time.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
call = { package = "call2", path = "../call2", features = ["test-support"] }
|
||||||
|
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||||
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||||
|
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
|
||||||
|
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||||
|
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||||
|
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||||
|
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
tree-sitter-markdown.workspace = true
|
454
crates/collab_ui2/src/channel_view.rs
Normal file
454
crates/collab_ui2/src/channel_view.rs
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
// use anyhow::{anyhow, Result};
|
||||||
|
// use call::report_call_event_for_channel;
|
||||||
|
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||||
|
// use client::{
|
||||||
|
// proto::{self, PeerId},
|
||||||
|
// Collaborator, ParticipantIndex,
|
||||||
|
// };
|
||||||
|
// use collections::HashMap;
|
||||||
|
// use editor::{CollaborationHub, Editor};
|
||||||
|
// use gpui::{
|
||||||
|
// actions,
|
||||||
|
// elements::{ChildView, Label},
|
||||||
|
// geometry::vector::Vector2F,
|
||||||
|
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||||
|
// ViewContext, ViewHandle,
|
||||||
|
// };
|
||||||
|
// use project::Project;
|
||||||
|
// use smallvec::SmallVec;
|
||||||
|
// use std::{
|
||||||
|
// any::{Any, TypeId},
|
||||||
|
// sync::Arc,
|
||||||
|
// };
|
||||||
|
// use util::ResultExt;
|
||||||
|
// use workspace::{
|
||||||
|
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||||
|
// register_followable_item,
|
||||||
|
// searchable::SearchableItemHandle,
|
||||||
|
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// actions!(channel_view, [Deploy]);
|
||||||
|
|
||||||
|
// pub fn init(cx: &mut AppContext) {
|
||||||
|
// register_followable_item::<ChannelView>(cx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct ChannelView {
|
||||||
|
// pub editor: ViewHandle<Editor>,
|
||||||
|
// project: ModelHandle<Project>,
|
||||||
|
// channel_store: ModelHandle<ChannelStore>,
|
||||||
|
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||||
|
// remote_id: Option<ViewId>,
|
||||||
|
// _editor_event_subscription: Subscription,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ChannelView {
|
||||||
|
// pub fn open(
|
||||||
|
// channel_id: ChannelId,
|
||||||
|
// workspace: ViewHandle<Workspace>,
|
||||||
|
// cx: &mut AppContext,
|
||||||
|
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
// let pane = workspace.read(cx).active_pane().clone();
|
||||||
|
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||||
|
// cx.spawn(|mut cx| async move {
|
||||||
|
// let channel_view = channel_view.await?;
|
||||||
|
// pane.update(&mut cx, |pane, cx| {
|
||||||
|
// report_call_event_for_channel(
|
||||||
|
// "open channel notes",
|
||||||
|
// channel_id,
|
||||||
|
// &workspace.read(cx).app_state().client,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||||
|
// });
|
||||||
|
// anyhow::Ok(channel_view)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn open_in_pane(
|
||||||
|
// channel_id: ChannelId,
|
||||||
|
// pane: ViewHandle<Pane>,
|
||||||
|
// workspace: ViewHandle<Workspace>,
|
||||||
|
// cx: &mut AppContext,
|
||||||
|
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
// let workspace = workspace.read(cx);
|
||||||
|
// let project = workspace.project().to_owned();
|
||||||
|
// let channel_store = ChannelStore::global(cx);
|
||||||
|
// let language_registry = workspace.app_state().languages.clone();
|
||||||
|
// let markdown = language_registry.language_for_name("Markdown");
|
||||||
|
// let channel_buffer =
|
||||||
|
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||||
|
|
||||||
|
// cx.spawn(|mut cx| async move {
|
||||||
|
// let channel_buffer = channel_buffer.await?;
|
||||||
|
// let markdown = markdown.await.log_err();
|
||||||
|
|
||||||
|
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
// buffer.buffer().update(cx, |buffer, cx| {
|
||||||
|
// buffer.set_language_registry(language_registry);
|
||||||
|
// if let Some(markdown) = markdown {
|
||||||
|
// buffer.set_language(Some(markdown), cx);
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
|
||||||
|
// pane.update(&mut cx, |pane, cx| {
|
||||||
|
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||||
|
|
||||||
|
// let existing_view = pane
|
||||||
|
// .items_of_type::<Self>()
|
||||||
|
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||||
|
|
||||||
|
// // If this channel buffer is already open in this pane, just return it.
|
||||||
|
// if let Some(existing_view) = existing_view.clone() {
|
||||||
|
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||||
|
// return existing_view;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let view = cx.add_view(|cx| {
|
||||||
|
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||||
|
// this.acknowledge_buffer_version(cx);
|
||||||
|
// this
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // If the pane contained a disconnected view for this channel buffer,
|
||||||
|
// // replace that.
|
||||||
|
// if let Some(existing_item) = existing_view {
|
||||||
|
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||||
|
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||||
|
// .detach();
|
||||||
|
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// view
|
||||||
|
// })
|
||||||
|
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn new(
|
||||||
|
// project: ModelHandle<Project>,
|
||||||
|
// channel_store: ModelHandle<ChannelStore>,
|
||||||
|
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) -> Self {
|
||||||
|
// let buffer = channel_buffer.read(cx).buffer();
|
||||||
|
// let editor = cx.add_view(|cx| {
|
||||||
|
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||||
|
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||||
|
// channel_buffer.clone(),
|
||||||
|
// )));
|
||||||
|
// editor.set_read_only(
|
||||||
|
// !channel_buffer
|
||||||
|
// .read(cx)
|
||||||
|
// .channel(cx)
|
||||||
|
// .is_some_and(|c| c.can_edit_notes()),
|
||||||
|
// );
|
||||||
|
// editor
|
||||||
|
// });
|
||||||
|
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||||
|
|
||||||
|
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||||
|
// .detach();
|
||||||
|
|
||||||
|
// Self {
|
||||||
|
// editor,
|
||||||
|
// project,
|
||||||
|
// channel_store,
|
||||||
|
// channel_buffer,
|
||||||
|
// remote_id: None,
|
||||||
|
// _editor_event_subscription,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||||
|
// self.channel_buffer.read(cx).channel(cx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn handle_channel_buffer_event(
|
||||||
|
// &mut self,
|
||||||
|
// _: ModelHandle<ChannelBuffer>,
|
||||||
|
// event: &ChannelBufferEvent,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) {
|
||||||
|
// match event {
|
||||||
|
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||||
|
// editor.set_read_only(true);
|
||||||
|
// cx.notify();
|
||||||
|
// }),
|
||||||
|
// ChannelBufferEvent::ChannelChanged => {
|
||||||
|
// self.editor.update(cx, |editor, cx| {
|
||||||
|
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||||
|
// cx.emit(editor::Event::TitleChanged);
|
||||||
|
// cx.notify()
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// ChannelBufferEvent::BufferEdited => {
|
||||||
|
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||||
|
// self.acknowledge_buffer_version(cx);
|
||||||
|
// } else {
|
||||||
|
// self.channel_store.update(cx, |store, cx| {
|
||||||
|
// let channel_buffer = self.channel_buffer.read(cx);
|
||||||
|
// store.notes_changed(
|
||||||
|
// channel_buffer.channel_id,
|
||||||
|
// channel_buffer.epoch(),
|
||||||
|
// &channel_buffer.buffer().read(cx).version(),
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||||
|
// self.channel_store.update(cx, |store, cx| {
|
||||||
|
// let channel_buffer = self.channel_buffer.read(cx);
|
||||||
|
// store.acknowledge_notes_version(
|
||||||
|
// channel_buffer.channel_id,
|
||||||
|
// channel_buffer.epoch(),
|
||||||
|
// &channel_buffer.buffer().read(cx).version(),
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// });
|
||||||
|
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||||
|
// buffer.acknowledge_buffer_version(cx);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Entity for ChannelView {
|
||||||
|
// type Event = editor::Event;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl View for ChannelView {
|
||||||
|
// fn ui_name() -> &'static str {
|
||||||
|
// "ChannelView"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
// if cx.is_self_focused() {
|
||||||
|
// self.acknowledge_buffer_version(cx);
|
||||||
|
// cx.focus(self.editor.as_any())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Item for ChannelView {
|
||||||
|
// fn act_as_type<'a>(
|
||||||
|
// &'a self,
|
||||||
|
// type_id: TypeId,
|
||||||
|
// self_handle: &'a ViewHandle<Self>,
|
||||||
|
// _: &'a AppContext,
|
||||||
|
// ) -> Option<&'a AnyViewHandle> {
|
||||||
|
// if type_id == TypeId::of::<Self>() {
|
||||||
|
// Some(self_handle)
|
||||||
|
// } else if type_id == TypeId::of::<Editor>() {
|
||||||
|
// Some(&self.editor)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn tab_content<V: 'static>(
|
||||||
|
// &self,
|
||||||
|
// _: Option<usize>,
|
||||||
|
// style: &theme::Tab,
|
||||||
|
// cx: &gpui::AppContext,
|
||||||
|
// ) -> AnyElement<V> {
|
||||||
|
// let label = if let Some(channel) = self.channel(cx) {
|
||||||
|
// match (
|
||||||
|
// channel.can_edit_notes(),
|
||||||
|
// self.channel_buffer.read(cx).is_connected(),
|
||||||
|
// ) {
|
||||||
|
// (true, true) => format!("#{}", channel.name),
|
||||||
|
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||||
|
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// format!("channel notes (disconnected)")
|
||||||
|
// };
|
||||||
|
// Label::new(label, style.label.to_owned()).into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||||
|
// Some(Self::new(
|
||||||
|
// self.project.clone(),
|
||||||
|
// self.channel_store.clone(),
|
||||||
|
// self.channel_buffer.clone(),
|
||||||
|
// cx,
|
||||||
|
// ))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
// self.editor
|
||||||
|
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.editor
|
||||||
|
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.editor
|
||||||
|
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||||
|
// Some(Box::new(self.editor.clone()))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn show_toolbar(&self) -> bool {
|
||||||
|
// true
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||||
|
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||||
|
// editor::Editor::to_item_events(event)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl FollowableItem for ChannelView {
|
||||||
|
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||||
|
// self.remote_id
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||||
|
// let channel_buffer = self.channel_buffer.read(cx);
|
||||||
|
// if !channel_buffer.is_connected() {
|
||||||
|
// return None;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Some(proto::view::Variant::ChannelView(
|
||||||
|
// proto::view::ChannelView {
|
||||||
|
// channel_id: channel_buffer.channel_id,
|
||||||
|
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||||
|
// self.editor.read(cx).to_state_proto(cx)
|
||||||
|
// {
|
||||||
|
// Some(proto)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn from_state_proto(
|
||||||
|
// pane: ViewHandle<workspace::Pane>,
|
||||||
|
// workspace: ViewHandle<workspace::Workspace>,
|
||||||
|
// remote_id: workspace::ViewId,
|
||||||
|
// state: &mut Option<proto::view::Variant>,
|
||||||
|
// cx: &mut AppContext,
|
||||||
|
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||||
|
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||||
|
// return None;
|
||||||
|
// };
|
||||||
|
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||||
|
// unreachable!()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||||
|
|
||||||
|
// Some(cx.spawn(|mut cx| async move {
|
||||||
|
// let this = open.await?;
|
||||||
|
|
||||||
|
// let task = this
|
||||||
|
// .update(&mut cx, |this, cx| {
|
||||||
|
// this.remote_id = Some(remote_id);
|
||||||
|
|
||||||
|
// if let Some(state) = state.editor {
|
||||||
|
// Some(this.editor.update(cx, |editor, cx| {
|
||||||
|
// editor.apply_update_proto(
|
||||||
|
// &this.project,
|
||||||
|
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||||
|
// selections: state.selections,
|
||||||
|
// pending_selection: state.pending_selection,
|
||||||
|
// scroll_top_anchor: state.scroll_top_anchor,
|
||||||
|
// scroll_x: state.scroll_x,
|
||||||
|
// scroll_y: state.scroll_y,
|
||||||
|
// ..Default::default()
|
||||||
|
// }),
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// }))
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||||
|
|
||||||
|
// if let Some(task) = task {
|
||||||
|
// task.await?;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(this)
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn add_event_to_update_proto(
|
||||||
|
// &self,
|
||||||
|
// event: &Self::Event,
|
||||||
|
// update: &mut Option<proto::update_view::Variant>,
|
||||||
|
// cx: &AppContext,
|
||||||
|
// ) -> bool {
|
||||||
|
// self.editor
|
||||||
|
// .read(cx)
|
||||||
|
// .add_event_to_update_proto(event, update, cx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn apply_update_proto(
|
||||||
|
// &mut self,
|
||||||
|
// project: &ModelHandle<Project>,
|
||||||
|
// message: proto::update_view::Variant,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||||
|
// self.editor.update(cx, |editor, cx| {
|
||||||
|
// editor.apply_update_proto(project, message, cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.editor.update(cx, |editor, cx| {
|
||||||
|
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||||
|
// Editor::should_unfollow_on_event(event, cx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||||
|
|
||||||
|
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||||
|
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||||
|
// self.0.read(cx).collaborators()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn user_participant_indices<'a>(
|
||||||
|
// &self,
|
||||||
|
// cx: &'a AppContext,
|
||||||
|
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||||
|
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||||
|
// }
|
||||||
|
// }
|
983
crates/collab_ui2/src/chat_panel.rs
Normal file
983
crates/collab_ui2/src/chat_panel.rs
Normal file
|
@ -0,0 +1,983 @@
|
||||||
|
// use crate::{
|
||||||
|
// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
|
||||||
|
// };
|
||||||
|
// use anyhow::Result;
|
||||||
|
// use call::ActiveCall;
|
||||||
|
// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||||
|
// use client::Client;
|
||||||
|
// use collections::HashMap;
|
||||||
|
// 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,
|
||||||
|
// };
|
||||||
|
// use language::LanguageRegistry;
|
||||||
|
// use menu::Confirm;
|
||||||
|
// use message_editor::MessageEditor;
|
||||||
|
// use project::Fs;
|
||||||
|
// use rich_text::RichText;
|
||||||
|
// use serde::{Deserialize, Serialize};
|
||||||
|
// use settings::SettingsStore;
|
||||||
|
// use std::sync::Arc;
|
||||||
|
// use theme::{IconButton, Theme};
|
||||||
|
// use time::{OffsetDateTime, UtcOffset};
|
||||||
|
// use util::{ResultExt, TryFutureExt};
|
||||||
|
// use workspace::{
|
||||||
|
// dock::{DockPosition, Panel},
|
||||||
|
// Workspace,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// mod message_editor;
|
||||||
|
|
||||||
|
// const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||||
|
// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||||
|
|
||||||
|
// pub struct ChatPanel {
|
||||||
|
// client: Arc<Client>,
|
||||||
|
// channel_store: ModelHandle<ChannelStore>,
|
||||||
|
// languages: Arc<LanguageRegistry>,
|
||||||
|
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||||
|
// message_list: ListState<ChatPanel>,
|
||||||
|
// input_editor: ViewHandle<MessageEditor>,
|
||||||
|
// channel_select: ViewHandle<Select>,
|
||||||
|
// local_timezone: UtcOffset,
|
||||||
|
// fs: Arc<dyn Fs>,
|
||||||
|
// width: Option<f32>,
|
||||||
|
// active: bool,
|
||||||
|
// pending_serialization: Task<Option<()>>,
|
||||||
|
// subscriptions: Vec<gpui::Subscription>,
|
||||||
|
// workspace: WeakViewHandle<Workspace>,
|
||||||
|
// is_scrolled_to_bottom: bool,
|
||||||
|
// has_focus: bool,
|
||||||
|
// markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Serialize, Deserialize)]
|
||||||
|
// struct SerializedChatPanel {
|
||||||
|
// width: Option<f32>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub enum Event {
|
||||||
|
// DockPositionChanged,
|
||||||
|
// Focus,
|
||||||
|
// Dismissed,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// actions!(
|
||||||
|
// chat_panel,
|
||||||
|
// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// pub fn init(cx: &mut AppContext) {
|
||||||
|
// cx.add_action(ChatPanel::send);
|
||||||
|
// cx.add_action(ChatPanel::load_more_messages);
|
||||||
|
// cx.add_action(ChatPanel::open_notes);
|
||||||
|
// cx.add_action(ChatPanel::join_call);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ChatPanel {
|
||||||
|
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||||
|
// let fs = workspace.app_state().fs.clone();
|
||||||
|
// let client = workspace.app_state().client.clone();
|
||||||
|
// let channel_store = ChannelStore::global(cx);
|
||||||
|
// let languages = workspace.app_state().languages.clone();
|
||||||
|
|
||||||
|
// let input_editor = cx.add_view(|cx| {
|
||||||
|
// 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,
|
||||||
|
// )
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// cx.add_view(|cx| {
|
||||||
|
// let mut this = Self {
|
||||||
|
// fs,
|
||||||
|
// client,
|
||||||
|
// channel_store,
|
||||||
|
// languages,
|
||||||
|
// 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(),
|
||||||
|
// workspace: workspace_handle,
|
||||||
|
// is_scrolled_to_bottom: true,
|
||||||
|
// active: false,
|
||||||
|
// width: None,
|
||||||
|
// markdown_data: Default::default(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let mut old_dock_position = this.position(cx);
|
||||||
|
// this.subscriptions
|
||||||
|
// .push(
|
||||||
|
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||||
|
// let new_dock_position = this.position(cx);
|
||||||
|
// if new_dock_position != old_dock_position {
|
||||||
|
// old_dock_position = new_dock_position;
|
||||||
|
// cx.emit(Event::DockPositionChanged);
|
||||||
|
// }
|
||||||
|
// cx.notify();
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// this.update_channel_count(cx);
|
||||||
|
// cx.observe(&this.channel_store, |this, _, cx| {
|
||||||
|
// this.update_channel_count(cx)
|
||||||
|
// })
|
||||||
|
// .detach();
|
||||||
|
|
||||||
|
// cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||||
|
// let selected_ix = channel_select.read(cx).selected_index();
|
||||||
|
|
||||||
|
// let selected_channel_id = this
|
||||||
|
// .channel_store
|
||||||
|
// .read(cx)
|
||||||
|
// .channel_at(selected_ix)
|
||||||
|
// .map(|e| e.id);
|
||||||
|
// if let Some(selected_channel_id) = selected_channel_id {
|
||||||
|
// this.select_channel(selected_channel_id, None, cx)
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .detach();
|
||||||
|
|
||||||
|
// this
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn is_scrolled_to_bottom(&self) -> bool {
|
||||||
|
// self.is_scrolled_to_bottom
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
|
||||||
|
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn load(
|
||||||
|
// workspace: WeakViewHandle<Workspace>,
|
||||||
|
// cx: AsyncAppContext,
|
||||||
|
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
// cx.spawn(|mut cx| async move {
|
||||||
|
// let serialized_panel = if let Some(panel) = cx
|
||||||
|
// .background()
|
||||||
|
// .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
|
||||||
|
// .await
|
||||||
|
// .log_err()
|
||||||
|
// .flatten()
|
||||||
|
// {
|
||||||
|
// Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// };
|
||||||
|
|
||||||
|
// workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
// let panel = Self::new(workspace, cx);
|
||||||
|
// if let Some(serialized_panel) = serialized_panel {
|
||||||
|
// panel.update(cx, |panel, cx| {
|
||||||
|
// panel.width = serialized_panel.width;
|
||||||
|
// cx.notify();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// panel
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
// let width = self.width;
|
||||||
|
// self.pending_serialization = cx.background().spawn(
|
||||||
|
// async move {
|
||||||
|
// KEY_VALUE_STORE
|
||||||
|
// .write_kvp(
|
||||||
|
// CHAT_PANEL_KEY.into(),
|
||||||
|
// serde_json::to_string(&SerializedChatPanel { width })?,
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
// anyhow::Ok(())
|
||||||
|
// }
|
||||||
|
// .log_err(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
// let channel_count = self.channel_store.read(cx).channel_count();
|
||||||
|
// self.channel_select.update(cx, |select, cx| {
|
||||||
|
// select.set_item_count(channel_count, cx);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_active_chat(&mut self, chat: ModelHandle<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;
|
||||||
|
// {
|
||||||
|
// self.markdown_data.clear();
|
||||||
|
// let chat = chat.read(cx);
|
||||||
|
// self.message_list.reset(chat.message_count());
|
||||||
|
|
||||||
|
// let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||||
|
// self.input_editor.update(cx, |editor, cx| {
|
||||||
|
// editor.set_channel(channel_id, channel_name, cx);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
// let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
||||||
|
// self.active_chat = Some((chat, subscription));
|
||||||
|
// self.acknowledge_last_message(cx);
|
||||||
|
// self.channel_select.update(cx, |select, cx| {
|
||||||
|
// if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
|
||||||
|
// select.set_selected_index(ix, cx);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn channel_did_change(
|
||||||
|
// &mut self,
|
||||||
|
// _: ModelHandle<ChannelChat>,
|
||||||
|
// event: &ChannelChatEvent,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) {
|
||||||
|
// match event {
|
||||||
|
// ChannelChatEvent::MessagesUpdated {
|
||||||
|
// old_range,
|
||||||
|
// new_count,
|
||||||
|
// } => {
|
||||||
|
// self.message_list.splice(old_range.clone(), *new_count);
|
||||||
|
// if self.active {
|
||||||
|
// self.acknowledge_last_message(cx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ChannelChatEvent::NewMessage {
|
||||||
|
// channel_id,
|
||||||
|
// message_id,
|
||||||
|
// } => {
|
||||||
|
// if !self.active {
|
||||||
|
// self.channel_store.update(cx, |store, cx| {
|
||||||
|
// store.new_message(*channel_id, *message_id, cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
|
||||||
|
// if self.active && self.is_scrolled_to_bottom {
|
||||||
|
// if let Some((chat, _)) = &self.active_chat {
|
||||||
|
// chat.update(cx, |chat, cx| {
|
||||||
|
// chat.acknowledge_last_message(cx);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
// .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()
|
||||||
|
// } else {
|
||||||
|
// Empty::new().into_any()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// messages.flex(1., true).into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// let (message, is_continuation, is_last, is_admin) = self
|
||||||
|
// .active_chat
|
||||||
|
// .as_ref()
|
||||||
|
// .unwrap()
|
||||||
|
// .0
|
||||||
|
// .update(cx, |active_chat, cx| {
|
||||||
|
// let is_admin = self
|
||||||
|
// .channel_store
|
||||||
|
// .read(cx)
|
||||||
|
// .is_channel_admin(active_chat.channel_id);
|
||||||
|
|
||||||
|
// let last_message = active_chat.message(ix.saturating_sub(1));
|
||||||
|
// let this_message = active_chat.message(ix).clone();
|
||||||
|
// let is_continuation = last_message.id != this_message.id
|
||||||
|
// && this_message.sender.id == last_message.sender.id;
|
||||||
|
|
||||||
|
// if let ChannelMessageId::Saved(id) = this_message.id {
|
||||||
|
// if this_message
|
||||||
|
// .mentions
|
||||||
|
// .iter()
|
||||||
|
// .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
|
||||||
|
// {
|
||||||
|
// active_chat.acknowledge_message(id);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// (
|
||||||
|
// this_message,
|
||||||
|
// is_continuation,
|
||||||
|
// active_chat.message_count() == ix + 1,
|
||||||
|
// is_admin,
|
||||||
|
// )
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// {
|
||||||
|
// Some(id)
|
||||||
|
// } else {
|
||||||
|
// 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,
|
||||||
|
// )
|
||||||
|
// .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()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_markdown_with_mentions(
|
||||||
|
// language_registry: &Arc<LanguageRegistry>,
|
||||||
|
// current_user_id: u64,
|
||||||
|
// message: &channel::ChannelMessage,
|
||||||
|
// ) -> RichText {
|
||||||
|
// let mentions = message
|
||||||
|
// .mentions
|
||||||
|
// .iter()
|
||||||
|
// .map(|(range, user_id)| rich_text::Mention {
|
||||||
|
// range: range.clone(),
|
||||||
|
// is_self_mention: *user_id == current_user_id,
|
||||||
|
// })
|
||||||
|
// .collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// 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()));
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// 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();
|
||||||
|
// })
|
||||||
|
// .aligned()
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||||
|
// let message = self
|
||||||
|
// .input_editor
|
||||||
|
// .update(cx, |editor, cx| editor.take_message(cx));
|
||||||
|
|
||||||
|
// if let Some(task) = chat
|
||||||
|
// .update(cx, |chat, cx| chat.send_message(message, cx))
|
||||||
|
// .log_err()
|
||||||
|
// {
|
||||||
|
// task.detach();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||||
|
// chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((chat, _)) = self.active_chat.as_ref() {
|
||||||
|
// chat.update(cx, |channel, cx| {
|
||||||
|
// if let Some(task) = channel.load_more_messages(cx) {
|
||||||
|
// task.detach();
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn select_channel(
|
||||||
|
// &mut self,
|
||||||
|
// selected_channel_id: u64,
|
||||||
|
// scroll_to_message_id: Option<u64>,
|
||||||
|
// cx: &mut ViewContext<ChatPanel>,
|
||||||
|
// ) -> Task<Result<()>> {
|
||||||
|
// let open_chat = self
|
||||||
|
// .active_chat
|
||||||
|
// .as_ref()
|
||||||
|
// .and_then(|(chat, _)| {
|
||||||
|
// (chat.read(cx).channel_id == selected_channel_id)
|
||||||
|
// .then(|| Task::ready(anyhow::Ok(chat.clone())))
|
||||||
|
// })
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
|
// self.channel_store.update(cx, |store, cx| {
|
||||||
|
// store.open_channel_chat(selected_channel_id, cx)
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
|
||||||
|
// cx.spawn(|this, mut cx| async move {
|
||||||
|
// let chat = open_chat.await?;
|
||||||
|
// this.update(&mut cx, |this, cx| {
|
||||||
|
// this.set_active_chat(chat.clone(), cx);
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// if let Some(message_id) = scroll_to_message_id {
|
||||||
|
// if let Some(item_ix) =
|
||||||
|
// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
|
||||||
|
// .await
|
||||||
|
// {
|
||||||
|
// this.update(&mut cx, |this, cx| {
|
||||||
|
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
|
||||||
|
// this.message_list.scroll_to(ListOffset {
|
||||||
|
// item_ix,
|
||||||
|
// offset_in_item: 0.,
|
||||||
|
// });
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((chat, _)) = &self.active_chat {
|
||||||
|
// let channel_id = chat.read(cx).channel_id;
|
||||||
|
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
// ChannelView::open(channel_id, workspace, cx).detach();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((chat, _)) = &self.active_chat {
|
||||||
|
// let channel_id = chat.read(cx).channel_id;
|
||||||
|
// ActiveCall::global(cx)
|
||||||
|
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_remove(
|
||||||
|
// message_id_to_remove: Option<u64>,
|
||||||
|
// cx: &mut ViewContext<'_, '_, ChatPanel>,
|
||||||
|
// theme: &Arc<Theme>,
|
||||||
|
// ) -> AnyElement<ChatPanel> {
|
||||||
|
// enum DeleteMessage {}
|
||||||
|
|
||||||
|
// message_id_to_remove
|
||||||
|
// .map(|id| {
|
||||||
|
// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
|
||||||
|
// let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
|
||||||
|
// render_icon_button(button_style, "icons/x.svg")
|
||||||
|
// .aligned()
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// .with_padding(Padding::uniform(2.))
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// this.remove_message(id, cx);
|
||||||
|
// })
|
||||||
|
// .flex_float()
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
|
// let style = theme.chat_panel.icon_button.default;
|
||||||
|
|
||||||
|
// Empty::new()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.button_width)
|
||||||
|
// .with_height(style.button_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_uniform_padding(2.)
|
||||||
|
// .flex_float()
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Entity for ChatPanel {
|
||||||
|
// type Event = Event;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 Panel for ChatPanel {
|
||||||
|
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||||
|
// settings::get::<ChatPanelSettings>(cx).dock
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||||
|
// matches!(position, DockPosition::Left | DockPosition::Right)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||||
|
// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
|
||||||
|
// settings.dock = Some(position)
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||||
|
// self.width
|
||||||
|
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.width = size;
|
||||||
|
// self.serialize(cx);
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.active = active;
|
||||||
|
// if active {
|
||||||
|
// self.acknowledge_last_message(cx);
|
||||||
|
// if !is_channels_feature_enabled(cx) {
|
||||||
|
// cx.emit(Event::Dismissed);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 has_focus(&self, _cx: &gpui::WindowContext) -> bool {
|
||||||
|
// self.has_focus
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn is_focus_event(event: &Self::Event) -> bool {
|
||||||
|
// matches!(event, Event::Focus)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn format_timestamp(
|
||||||
|
// mut timestamp: OffsetDateTime,
|
||||||
|
// mut now: OffsetDateTime,
|
||||||
|
// local_timezone: UtcOffset,
|
||||||
|
// ) -> String {
|
||||||
|
// timestamp = timestamp.to_offset(local_timezone);
|
||||||
|
// now = now.to_offset(local_timezone);
|
||||||
|
|
||||||
|
// let today = now.date();
|
||||||
|
// let date = timestamp.date();
|
||||||
|
// let mut hour = timestamp.hour();
|
||||||
|
// let mut part = "am";
|
||||||
|
// if hour > 12 {
|
||||||
|
// hour -= 12;
|
||||||
|
// part = "pm";
|
||||||
|
// }
|
||||||
|
// if date == today {
|
||||||
|
// format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||||
|
// } else if date.next_day() == Some(today) {
|
||||||
|
// format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||||
|
// } else {
|
||||||
|
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
|
||||||
|
// Svg::new(svg_path)
|
||||||
|
// .with_color(style.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.button_width)
|
||||||
|
// .with_height(style.button_width)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use super::*;
|
||||||
|
// use gpui::fonts::HighlightStyle;
|
||||||
|
// use pretty_assertions::assert_eq;
|
||||||
|
// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
|
||||||
|
// use util::test::marked_text_ranges;
|
||||||
|
|
||||||
|
// #[gpui::test]
|
||||||
|
// fn test_render_markdown_with_mentions() {
|
||||||
|
// let language_registry = Arc::new(LanguageRegistry::test());
|
||||||
|
// let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
|
||||||
|
// let message = channel::ChannelMessage {
|
||||||
|
// id: ChannelMessageId::Saved(0),
|
||||||
|
// body,
|
||||||
|
// timestamp: OffsetDateTime::now_utc(),
|
||||||
|
// sender: Arc::new(client::User {
|
||||||
|
// github_login: "fgh".into(),
|
||||||
|
// avatar: None,
|
||||||
|
// id: 103,
|
||||||
|
// }),
|
||||||
|
// nonce: 5,
|
||||||
|
// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||||
|
|
||||||
|
// // Note that the "'" was replaced with ’ due to smart punctuation.
|
||||||
|
// let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
|
||||||
|
// assert_eq!(message.text, body);
|
||||||
|
// assert_eq!(
|
||||||
|
// message.highlights,
|
||||||
|
// vec![
|
||||||
|
// (
|
||||||
|
// ranges[0].clone(),
|
||||||
|
// HighlightStyle {
|
||||||
|
// italic: Some(true),
|
||||||
|
// ..Default::default()
|
||||||
|
// }
|
||||||
|
// .into()
|
||||||
|
// ),
|
||||||
|
// (ranges[1].clone(), Highlight::Mention),
|
||||||
|
// (
|
||||||
|
// ranges[2].clone(),
|
||||||
|
// HighlightStyle {
|
||||||
|
// weight: Some(gpui::fonts::Weight::BOLD),
|
||||||
|
// ..Default::default()
|
||||||
|
// }
|
||||||
|
// .into()
|
||||||
|
// ),
|
||||||
|
// (ranges[3].clone(), Highlight::SelfMention)
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
// assert_eq!(
|
||||||
|
// message.regions,
|
||||||
|
// vec![
|
||||||
|
// RenderedRegion {
|
||||||
|
// background_kind: Some(BackgroundKind::Mention),
|
||||||
|
// link_url: None
|
||||||
|
// },
|
||||||
|
// RenderedRegion {
|
||||||
|
// background_kind: Some(BackgroundKind::SelfMention),
|
||||||
|
// link_url: None
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal file
313
crates/collab_ui2/src/chat_panel/message_editor.rs
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
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 language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use project::search::SearchQuery;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
||||||
|
"@[-_\\w]+",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Default::default(),
|
||||||
|
Default::default()
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageEditor {
|
||||||
|
pub editor: ViewHandle<Editor>,
|
||||||
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
users: HashMap<String, UserId>,
|
||||||
|
mentions: Vec<UserId>,
|
||||||
|
mentions_task: Option<Task<()>>,
|
||||||
|
channel_id: Option<ChannelId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageEditor {
|
||||||
|
pub fn new(
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
editor: ViewHandle<Editor>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer = editor
|
||||||
|
.read(cx)
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()
|
||||||
|
.expect("message editor must be singleton");
|
||||||
|
|
||||||
|
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||||
|
|
||||||
|
let markdown = language_registry.language_for_name("Markdown");
|
||||||
|
cx.app_context()
|
||||||
|
.spawn(|mut cx| async move {
|
||||||
|
let markdown = markdown.await?;
|
||||||
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.set_language(Some(markdown), cx)
|
||||||
|
});
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
channel_store,
|
||||||
|
users: HashMap::default(),
|
||||||
|
channel_id: None,
|
||||||
|
mentions: Vec::new(),
|
||||||
|
mentions_task: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_channel(
|
||||||
|
&mut self,
|
||||||
|
channel_id: u64,
|
||||||
|
channel_name: Option<String>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
if let Some(channel_name) = channel_name {
|
||||||
|
editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
|
||||||
|
} else {
|
||||||
|
editor.set_placeholder_text(format!("Message Channel"), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.channel_id = Some(channel_id);
|
||||||
|
self.refresh_users(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(channel_id) = self.channel_id {
|
||||||
|
let members = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.get_channel_member_details(channel_id, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let members = members.await?;
|
||||||
|
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
|
||||||
|
self.users.clear();
|
||||||
|
self.users.extend(
|
||||||
|
members
|
||||||
|
.into_iter()
|
||||||
|
.map(|member| (member.user.github_login.clone(), member.user.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
let highlights = editor.text_highlights::<Self>(cx);
|
||||||
|
let text = editor.text(cx);
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let mentions = if let Some((_, ranges)) = highlights {
|
||||||
|
ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| range.to_offset(&snapshot))
|
||||||
|
.zip(self.mentions.iter().copied())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.clear(cx);
|
||||||
|
self.mentions.clear();
|
||||||
|
|
||||||
|
MessageParams { text, mentions }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_buffer_event(
|
||||||
|
&mut self,
|
||||||
|
buffer: ModelHandle<Buffer>,
|
||||||
|
event: &language::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let language::Event::Reparsed | language::Event::Edited = event {
|
||||||
|
let buffer = buffer.read(cx).snapshot();
|
||||||
|
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||||
|
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
|
||||||
|
Self::find_mentions(this, buffer, cx).await;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_mentions(
|
||||||
|
this: WeakViewHandle<MessageEditor>,
|
||||||
|
buffer: BufferSnapshot,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) {
|
||||||
|
let (buffer, ranges) = cx
|
||||||
|
.background()
|
||||||
|
.spawn(async move {
|
||||||
|
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
|
||||||
|
(buffer, ranges)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let mut anchor_ranges = Vec::new();
|
||||||
|
let mut mentioned_user_ids = Vec::new();
|
||||||
|
let mut text = String::new();
|
||||||
|
|
||||||
|
this.editor.update(cx, |editor, cx| {
|
||||||
|
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
for range in ranges {
|
||||||
|
text.clear();
|
||||||
|
text.extend(buffer.text_for_range(range.clone()));
|
||||||
|
if let Some(username) = text.strip_prefix("@") {
|
||||||
|
if let Some(user_id) = this.users.get(username) {
|
||||||
|
let start = multi_buffer.anchor_after(range.start);
|
||||||
|
let end = multi_buffer.anchor_after(range.end);
|
||||||
|
|
||||||
|
mentioned_user_ids.push(*user_id);
|
||||||
|
anchor_ranges.push(start..end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.clear_highlights::<Self>(cx);
|
||||||
|
editor.highlight_text::<Self>(
|
||||||
|
anchor_ranges,
|
||||||
|
theme::current(cx).chat_panel.rich_text.mention_highlight,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mentions = mentioned_user_ids;
|
||||||
|
this.mentions_task.take();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use client::{Client, User, UserStore};
|
||||||
|
use gpui::{TestAppContext, WindowHandle};
|
||||||
|
use language::{Language, LanguageConfig};
|
||||||
|
use rpc::proto;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use util::{http::FakeHttpClient, test::marked_text_ranges};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||||
|
let editor = init_test(cx);
|
||||||
|
let editor = editor.root(cx);
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_members(
|
||||||
|
vec![
|
||||||
|
ChannelMembership {
|
||||||
|
user: Arc::new(User {
|
||||||
|
github_login: "a-b".into(),
|
||||||
|
id: 101,
|
||||||
|
avatar: None,
|
||||||
|
}),
|
||||||
|
kind: proto::channel_member::Kind::Member,
|
||||||
|
role: proto::ChannelRole::Member,
|
||||||
|
},
|
||||||
|
ChannelMembership {
|
||||||
|
user: Arc::new(User {
|
||||||
|
github_login: "C_D".into(),
|
||||||
|
id: 102,
|
||||||
|
avatar: None,
|
||||||
|
}),
|
||||||
|
kind: proto::channel_member::Kind::Member,
|
||||||
|
role: proto::ChannelRole::Member,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||||
|
assert_eq!(
|
||||||
|
editor.take_message(cx),
|
||||||
|
MessageParams {
|
||||||
|
text,
|
||||||
|
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
|
||||||
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
let http = FakeHttpClient::with_404_response();
|
||||||
|
let client = Client::new(http.clone(), cx);
|
||||||
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||||
|
cx.set_global(SettingsStore::test(cx));
|
||||||
|
theme::init((), cx);
|
||||||
|
language::init(cx);
|
||||||
|
editor::init(cx);
|
||||||
|
client::init(&client, cx);
|
||||||
|
channel::init(&client, user_store, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let language_registry = Arc::new(LanguageRegistry::test());
|
||||||
|
language_registry.add(Arc::new(Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Markdown".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_markdown::language()),
|
||||||
|
)));
|
||||||
|
|
||||||
|
let editor = cx.add_window(|cx| {
|
||||||
|
MessageEditor::new(
|
||||||
|
language_registry,
|
||||||
|
ChannelStore::global(cx),
|
||||||
|
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
editor
|
||||||
|
}
|
||||||
|
}
|
3574
crates/collab_ui2/src/collab_panel.rs
Normal file
3574
crates/collab_ui2/src/collab_panel.rs
Normal file
File diff suppressed because it is too large
Load diff
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal file
717
crates/collab_ui2/src/collab_panel/channel_modal.rs
Normal file
|
@ -0,0 +1,717 @@
|
||||||
|
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||||
|
use client::{
|
||||||
|
proto::{self, ChannelRole, ChannelVisibility},
|
||||||
|
User, UserId, UserStore,
|
||||||
|
};
|
||||||
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::*,
|
||||||
|
platform::{CursorStyle, MouseButton},
|
||||||
|
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||||
|
ViewHandle,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::TryFutureExt;
|
||||||
|
use workspace::Modal;
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
channel_modal,
|
||||||
|
[
|
||||||
|
SelectNextControl,
|
||||||
|
ToggleMode,
|
||||||
|
ToggleMemberAdmin,
|
||||||
|
RemoveMember
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
Picker::<ChannelModalDelegate>::init(cx);
|
||||||
|
cx.add_action(ChannelModal::toggle_mode);
|
||||||
|
cx.add_action(ChannelModal::toggle_member_admin);
|
||||||
|
cx.add_action(ChannelModal::remove_member);
|
||||||
|
cx.add_action(ChannelModal::dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChannelModal {
|
||||||
|
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||||
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
has_focus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelModal {
|
||||||
|
pub fn new(
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
mode: Mode,
|
||||||
|
members: Vec<ChannelMembership>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||||
|
let picker = cx.add_view(|cx| {
|
||||||
|
Picker::new(
|
||||||
|
ChannelModalDelegate {
|
||||||
|
matching_users: Vec::new(),
|
||||||
|
matching_member_indices: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
user_store: user_store.clone(),
|
||||||
|
channel_store: channel_store.clone(),
|
||||||
|
channel_id,
|
||||||
|
match_candidates: Vec::new(),
|
||||||
|
members,
|
||||||
|
mode,
|
||||||
|
context_menu: cx.add_view(|cx| {
|
||||||
|
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||||
|
menu.set_position_mode(OverlayPositionMode::Local);
|
||||||
|
menu
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||||
|
|
||||||
|
let has_focus = picker.read(cx).has_focus();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
picker,
|
||||||
|
channel_store,
|
||||||
|
channel_id,
|
||||||
|
has_focus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||||
|
let mode = match self.picker.read(cx).delegate().mode {
|
||||||
|
Mode::ManageMembers => Mode::InviteMembers,
|
||||||
|
Mode::InviteMembers => Mode::ManageMembers,
|
||||||
|
};
|
||||||
|
self.set_mode(mode, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||||
|
let channel_store = self.channel_store.clone();
|
||||||
|
let channel_id = self.channel_id;
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if mode == Mode::ManageMembers {
|
||||||
|
let mut members = channel_store
|
||||||
|
.update(&mut cx, |channel_store, cx| {
|
||||||
|
channel_store.get_channel_member_details(channel_id, cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.picker
|
||||||
|
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.picker.update(cx, |picker, cx| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.mode = mode;
|
||||||
|
delegate.selected_index = 0;
|
||||||
|
picker.set_query("", cx);
|
||||||
|
picker.update_matches(picker.query(cx), cx);
|
||||||
|
cx.notify()
|
||||||
|
});
|
||||||
|
cx.notify()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||||
|
self.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||||
|
self.picker.update(cx, |picker, cx| {
|
||||||
|
picker.delegate_mut().remove_selected_member(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ChannelModal {
|
||||||
|
type Event = PickerEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ChannelModal {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ChannelModal"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||||
|
|
||||||
|
let mode = self.picker.read(cx).delegate().mode;
|
||||||
|
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||||
|
return Empty::new().into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
enum InviteMembers {}
|
||||||
|
enum ManageMembers {}
|
||||||
|
|
||||||
|
fn render_mode_button<T: 'static>(
|
||||||
|
mode: Mode,
|
||||||
|
text: &'static str,
|
||||||
|
current_mode: Mode,
|
||||||
|
theme: &theme::TabbedModal,
|
||||||
|
cx: &mut ViewContext<ChannelModal>,
|
||||||
|
) -> AnyElement<ChannelModal> {
|
||||||
|
let active = mode == current_mode;
|
||||||
|
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||||
|
let contained_text = theme.tab_button.style_for(active, state);
|
||||||
|
Label::new(text, contained_text.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(contained_text.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
if !active {
|
||||||
|
this.set_mode(mode, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_visibility(
|
||||||
|
channel_id: ChannelId,
|
||||||
|
visibility: ChannelVisibility,
|
||||||
|
theme: &theme::TabbedModal,
|
||||||
|
cx: &mut ViewContext<ChannelModal>,
|
||||||
|
) -> AnyElement<ChannelModal> {
|
||||||
|
enum TogglePublic {}
|
||||||
|
|
||||||
|
if visibility == ChannelVisibility::Members {
|
||||||
|
return Flex::row()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
let style = theme.visibility_toggle.style_for(state);
|
||||||
|
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.channel_store
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
ChannelVisibility::Public,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.into_any();
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
let style = theme.visibility_toggle.style_for(state);
|
||||||
|
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.channel_store
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
ChannelVisibility::Members,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.with_spacing(14.0)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||||
|
let style = theme.channel_link.style_for(state);
|
||||||
|
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
if let Some(channel) =
|
||||||
|
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||||
|
{
|
||||||
|
let item = ClipboardItem::new(channel.link());
|
||||||
|
cx.write_to_clipboard(item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.title.container.clone()),
|
||||||
|
)
|
||||||
|
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||||
|
.with_child(Flex::row().with_children([
|
||||||
|
render_mode_button::<InviteMembers>(
|
||||||
|
Mode::InviteMembers,
|
||||||
|
"Invite members",
|
||||||
|
mode,
|
||||||
|
theme,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
render_mode_button::<ManageMembers>(
|
||||||
|
Mode::ManageMembers,
|
||||||
|
"Manage members",
|
||||||
|
mode,
|
||||||
|
theme,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
]))
|
||||||
|
.expanded()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.header),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
ChildView::new(&self.picker, cx)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.body),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_max_height(theme.max_height)
|
||||||
|
.with_max_width(theme.max_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.modal)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = true;
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.picker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modal for ChannelModal {
|
||||||
|
fn has_focus(&self) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||||
|
match event {
|
||||||
|
PickerEvent::Dismiss => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
|
pub enum Mode {
|
||||||
|
ManageMembers,
|
||||||
|
InviteMembers,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChannelModalDelegate {
|
||||||
|
matching_users: Vec<Arc<User>>,
|
||||||
|
matching_member_indices: Vec<usize>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
selected_index: usize,
|
||||||
|
mode: Mode,
|
||||||
|
match_candidates: Vec<StringMatchCandidate>,
|
||||||
|
members: Vec<ChannelMembership>,
|
||||||
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Search collaborator by username...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
match self.mode {
|
||||||
|
Mode::ManageMembers => self.matching_member_indices.len(),
|
||||||
|
Mode::InviteMembers => self.matching_users.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
|
match self.mode {
|
||||||
|
Mode::ManageMembers => {
|
||||||
|
self.match_candidates.clear();
|
||||||
|
self.match_candidates
|
||||||
|
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||||
|
StringMatchCandidate {
|
||||||
|
id,
|
||||||
|
string: member.user.github_login.clone(),
|
||||||
|
char_bag: member.user.github_login.chars().collect(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let matches = cx.background().block(match_strings(
|
||||||
|
&self.match_candidates,
|
||||||
|
&query,
|
||||||
|
true,
|
||||||
|
usize::MAX,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background().clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
picker
|
||||||
|
.update(&mut cx, |picker, cx| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.matching_member_indices.clear();
|
||||||
|
delegate
|
||||||
|
.matching_member_indices
|
||||||
|
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Mode::InviteMembers => {
|
||||||
|
let search_users = self
|
||||||
|
.user_store
|
||||||
|
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
async {
|
||||||
|
let users = search_users.await?;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
let delegate = picker.delegate_mut();
|
||||||
|
delegate.matching_users = users;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::ManageMembers => {
|
||||||
|
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||||
|
}
|
||||||
|
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||||
|
Some(proto::channel_member::Kind::Invitee) => {
|
||||||
|
self.remove_selected_member(cx);
|
||||||
|
}
|
||||||
|
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||||
|
self.invite_member(selected_user, cx)
|
||||||
|
}
|
||||||
|
Some(proto::channel_member::Kind::Member) => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> AnyElement<Picker<Self>> {
|
||||||
|
let full_theme = &theme::current(cx);
|
||||||
|
let theme = &full_theme.collab_panel.channel_modal;
|
||||||
|
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||||
|
let (user, role) = self.user_at_index(ix).unwrap();
|
||||||
|
let request_status = self.member_status(user.id, cx);
|
||||||
|
|
||||||
|
let style = tabbed_modal
|
||||||
|
.picker
|
||||||
|
.item
|
||||||
|
.in_state(selected)
|
||||||
|
.style_for(mouse_state);
|
||||||
|
|
||||||
|
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||||
|
|
||||||
|
let mut result = Flex::row()
|
||||||
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
|
Image::from_data(avatar)
|
||||||
|
.with_style(theme.contact_avatar)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Label::new(user.github_login.clone(), style.label.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.contact_username)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.with_children({
|
||||||
|
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||||
|
|| {
|
||||||
|
Label::new("Invited", theme.member_tag.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_tag.container)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||||
|
Some(
|
||||||
|
Label::new("Admin", theme.member_tag.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_tag.container)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||||
|
Some(
|
||||||
|
Label::new("Guest", theme.member_tag.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_tag.container)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.with_children({
|
||||||
|
let svg = match self.mode {
|
||||||
|
Mode::ManageMembers => Some(
|
||||||
|
Svg::new("icons/ellipsis.svg")
|
||||||
|
.with_color(theme.member_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.member_icon.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.member_icon.button_width)
|
||||||
|
.with_height(theme.member_icon.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_icon.container),
|
||||||
|
),
|
||||||
|
Mode::InviteMembers => match request_status {
|
||||||
|
Some(proto::channel_member::Kind::Member) => Some(
|
||||||
|
Svg::new("icons/check.svg")
|
||||||
|
.with_color(theme.member_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.member_icon.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.member_icon.button_width)
|
||||||
|
.with_height(theme.member_icon.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_icon.container),
|
||||||
|
),
|
||||||
|
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||||
|
Svg::new("icons/check.svg")
|
||||||
|
.with_color(theme.invitee_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.invitee_icon.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.invitee_icon.button_width)
|
||||||
|
.with_height(theme.invitee_icon.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.invitee_icon.container),
|
||||||
|
),
|
||||||
|
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_height(tabbed_modal.row_height)
|
||||||
|
.into_any();
|
||||||
|
|
||||||
|
if selected {
|
||||||
|
result = Stack::new()
|
||||||
|
.with_child(result)
|
||||||
|
.with_child(
|
||||||
|
ChildView::new(&self.context_menu, cx)
|
||||||
|
.aligned()
|
||||||
|
.top()
|
||||||
|
.right(),
|
||||||
|
)
|
||||||
|
.into_any();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelModalDelegate {
|
||||||
|
fn member_status(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<proto::channel_member::Kind> {
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
|
||||||
|
.or_else(|| {
|
||||||
|
self.channel_store
|
||||||
|
.read(cx)
|
||||||
|
.has_pending_channel_invite(self.channel_id, user_id)
|
||||||
|
.then_some(proto::channel_member::Kind::Invitee)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||||
|
match self.mode {
|
||||||
|
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||||
|
let channel_membership = self.members.get(*ix)?;
|
||||||
|
Some((
|
||||||
|
channel_membership.user.clone(),
|
||||||
|
Some(channel_membership.role),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||||
|
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||||
|
let new_role = if role == Some(ChannelRole::Admin) {
|
||||||
|
ChannelRole::Member
|
||||||
|
} else {
|
||||||
|
ChannelRole::Admin
|
||||||
|
};
|
||||||
|
let update = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
update.await?;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
let this = picker.delegate_mut();
|
||||||
|
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||||
|
member.role = new_role;
|
||||||
|
}
|
||||||
|
cx.focus_self();
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||||
|
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||||
|
let user_id = user.id;
|
||||||
|
let update = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.remove_member(self.channel_id, user_id, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
update.await?;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
let this = picker.delegate_mut();
|
||||||
|
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||||
|
this.members.remove(ix);
|
||||||
|
this.matching_member_indices.retain_mut(|member_ix| {
|
||||||
|
if *member_ix == ix {
|
||||||
|
return false;
|
||||||
|
} else if *member_ix > ix {
|
||||||
|
*member_ix -= 1;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected_index = this
|
||||||
|
.selected_index
|
||||||
|
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||||
|
|
||||||
|
cx.focus_self();
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||||
|
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
invite_member.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let new_member = ChannelMembership {
|
||||||
|
user,
|
||||||
|
kind: proto::channel_member::Kind::Invitee,
|
||||||
|
role: ChannelRole::Member,
|
||||||
|
};
|
||||||
|
let members = &mut this.delegate_mut().members;
|
||||||
|
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||||
|
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.context_menu.update(cx, |context_menu, cx| {
|
||||||
|
context_menu.show(
|
||||||
|
Default::default(),
|
||||||
|
AnchorCorner::TopRight,
|
||||||
|
vec![
|
||||||
|
ContextMenuItem::action("Remove", RemoveMember),
|
||||||
|
ContextMenuItem::action(
|
||||||
|
if role == ChannelRole::Admin {
|
||||||
|
"Make non-admin"
|
||||||
|
} else {
|
||||||
|
"Make admin"
|
||||||
|
},
|
||||||
|
ToggleMemberAdmin,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal file
261
crates/collab_ui2/src/collab_panel/contact_finder.rs
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
use client::{ContactRequestStatus, User, UserStore};
|
||||||
|
use gpui::{
|
||||||
|
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use util::TryFutureExt;
|
||||||
|
use workspace::Modal;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
Picker::<ContactFinderDelegate>::init(cx);
|
||||||
|
cx.add_action(ContactFinder::dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContactFinder {
|
||||||
|
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||||
|
has_focus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactFinder {
|
||||||
|
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let picker = cx.add_view(|cx| {
|
||||||
|
Picker::new(
|
||||||
|
ContactFinderDelegate {
|
||||||
|
user_store,
|
||||||
|
potential_contacts: Arc::from([]),
|
||||||
|
selected_index: 0,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
picker,
|
||||||
|
has_focus: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||||
|
self.picker.update(cx, |picker, cx| {
|
||||||
|
picker.set_query(query, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ContactFinder {
|
||||||
|
type Event = PickerEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ContactFinder {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ContactFinder"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
let full_theme = &theme::current(cx);
|
||||||
|
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||||
|
|
||||||
|
fn render_mode_button(
|
||||||
|
text: &'static str,
|
||||||
|
theme: &theme::TabbedModal,
|
||||||
|
_cx: &mut ViewContext<ContactFinder>,
|
||||||
|
) -> AnyElement<ContactFinder> {
|
||||||
|
let contained_text = &theme.tab_button.active_state().default;
|
||||||
|
Label::new(text, contained_text.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(contained_text.container.clone())
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new("Contacts", theme.title.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.title.container.clone()),
|
||||||
|
)
|
||||||
|
.with_child(Flex::row().with_children([render_mode_button(
|
||||||
|
"Invite new contacts",
|
||||||
|
&theme,
|
||||||
|
cx,
|
||||||
|
)]))
|
||||||
|
.expanded()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.header),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
ChildView::new(&self.picker, cx)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.body),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_max_height(theme.max_height)
|
||||||
|
.with_max_width(theme.max_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.modal)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = true;
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.picker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
self.has_focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modal for ContactFinder {
|
||||||
|
fn has_focus(&self) -> bool {
|
||||||
|
self.has_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||||
|
match event {
|
||||||
|
PickerEvent::Dismiss => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContactFinderDelegate {
|
||||||
|
potential_contacts: Arc<[Arc<User>]>,
|
||||||
|
user_store: ModelHandle<UserStore>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for ContactFinderDelegate {
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Search collaborator by username...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.potential_contacts.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
|
let search_users = self
|
||||||
|
.user_store
|
||||||
|
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||||
|
|
||||||
|
cx.spawn(|picker, mut cx| async move {
|
||||||
|
async {
|
||||||
|
let potential_contacts = search_users.await?;
|
||||||
|
picker.update(&mut cx, |picker, cx| {
|
||||||
|
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
if let Some(user) = self.potential_contacts.get(self.selected_index) {
|
||||||
|
let user_store = self.user_store.read(cx);
|
||||||
|
match user_store.contact_request_status(user) {
|
||||||
|
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||||
|
self.user_store
|
||||||
|
.update(cx, |store, cx| store.request_contact(user.id, cx))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
ContactRequestStatus::RequestSent => {
|
||||||
|
self.user_store
|
||||||
|
.update(cx, |store, cx| store.remove_contact(user.id, cx))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> AnyElement<Picker<Self>> {
|
||||||
|
let full_theme = &theme::current(cx);
|
||||||
|
let theme = &full_theme.collab_panel.contact_finder;
|
||||||
|
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||||
|
let user = &self.potential_contacts[ix];
|
||||||
|
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||||
|
|
||||||
|
let icon_path = match request_status {
|
||||||
|
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
|
||||||
|
Some("icons/check.svg")
|
||||||
|
}
|
||||||
|
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||||
|
ContactRequestStatus::RequestAccepted => None,
|
||||||
|
};
|
||||||
|
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||||
|
&theme.disabled_contact_button
|
||||||
|
} else {
|
||||||
|
&theme.contact_button
|
||||||
|
};
|
||||||
|
let style = tabbed_modal
|
||||||
|
.picker
|
||||||
|
.item
|
||||||
|
.in_state(selected)
|
||||||
|
.style_for(mouse_state);
|
||||||
|
Flex::row()
|
||||||
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
|
Image::from_data(avatar)
|
||||||
|
.with_style(theme.contact_avatar)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Label::new(user.github_login.clone(), style.label.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.contact_username)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.with_children(icon_path.map(|icon_path| {
|
||||||
|
Svg::new(icon_path)
|
||||||
|
.with_color(button_style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(button_style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_width(button_style.button_width)
|
||||||
|
.with_height(button_style.button_width)
|
||||||
|
.aligned()
|
||||||
|
.flex_float()
|
||||||
|
}))
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_height(tabbed_modal.row_height)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
1367
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
1367
crates/collab_ui2/src/collab_titlebar_item.rs
Normal file
File diff suppressed because it is too large
Load diff
154
crates/collab_ui2/src/collab_ui.rs
Normal file
154
crates/collab_ui2/src/collab_ui.rs
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
pub mod channel_view;
|
||||||
|
pub mod chat_panel;
|
||||||
|
pub mod collab_panel;
|
||||||
|
mod collab_titlebar_item;
|
||||||
|
mod face_pile;
|
||||||
|
pub mod notification_panel;
|
||||||
|
pub mod notifications;
|
||||||
|
mod panel_settings;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub use collab_panel::CollabPanel;
|
||||||
|
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||||
|
use gpui::AppContext;
|
||||||
|
pub use panel_settings::{
|
||||||
|
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use workspace::AppState;
|
||||||
|
|
||||||
|
// actions!(
|
||||||
|
// collab,
|
||||||
|
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||||
|
// );
|
||||||
|
|
||||||
|
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
CollaborationPanelSettings::register(cx);
|
||||||
|
ChatPanelSettings::register(cx);
|
||||||
|
NotificationPanelSettings::register(cx);
|
||||||
|
|
||||||
|
// vcs_menu::init(cx);
|
||||||
|
collab_titlebar_item::init(cx);
|
||||||
|
collab_panel::init(cx);
|
||||||
|
// chat_panel::init(cx);
|
||||||
|
// notifications::init(&app_state, cx);
|
||||||
|
|
||||||
|
// cx.add_global_action(toggle_screen_sharing);
|
||||||
|
// cx.add_global_action(toggle_mute);
|
||||||
|
// cx.add_global_action(toggle_deafen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||||
|
// let call = ActiveCall::global(cx).read(cx);
|
||||||
|
// if let Some(room) = call.room().cloned() {
|
||||||
|
// let client = call.client();
|
||||||
|
// let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||||
|
// if room.is_screen_sharing() {
|
||||||
|
// report_call_event_for_room(
|
||||||
|
// "disable screen share",
|
||||||
|
// room.id(),
|
||||||
|
// room.channel_id(),
|
||||||
|
// &client,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// Task::ready(room.unshare_screen(cx))
|
||||||
|
// } else {
|
||||||
|
// report_call_event_for_room(
|
||||||
|
// "enable screen share",
|
||||||
|
// room.id(),
|
||||||
|
// room.channel_id(),
|
||||||
|
// &client,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// room.share_screen(cx)
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// toggle_screen_sharing.detach_and_log_err(cx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||||
|
// let call = ActiveCall::global(cx).read(cx);
|
||||||
|
// if let Some(room) = call.room().cloned() {
|
||||||
|
// let client = call.client();
|
||||||
|
// room.update(cx, |room, cx| {
|
||||||
|
// let operation = if room.is_muted(cx) {
|
||||||
|
// "enable microphone"
|
||||||
|
// } else {
|
||||||
|
// "disable microphone"
|
||||||
|
// };
|
||||||
|
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||||
|
|
||||||
|
// room.toggle_mute(cx)
|
||||||
|
// })
|
||||||
|
// .map(|task| task.detach_and_log_err(cx))
|
||||||
|
// .log_err();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||||
|
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||||
|
// room.update(cx, Room::toggle_deafen)
|
||||||
|
// .map(|task| task.detach_and_log_err(cx))
|
||||||
|
// .log_err();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn notification_window_options(
|
||||||
|
// screen: Rc<dyn Screen>,
|
||||||
|
// window_size: Vector2F,
|
||||||
|
// ) -> WindowOptions<'static> {
|
||||||
|
// const NOTIFICATION_PADDING: f32 = 16.;
|
||||||
|
|
||||||
|
// let screen_bounds = screen.content_bounds();
|
||||||
|
// WindowOptions {
|
||||||
|
// bounds: WindowBounds::Fixed(RectF::new(
|
||||||
|
// screen_bounds.upper_right()
|
||||||
|
// + vec2f(
|
||||||
|
// -NOTIFICATION_PADDING - window_size.x(),
|
||||||
|
// NOTIFICATION_PADDING,
|
||||||
|
// ),
|
||||||
|
// window_size,
|
||||||
|
// )),
|
||||||
|
// titlebar: None,
|
||||||
|
// center: false,
|
||||||
|
// focus: false,
|
||||||
|
// show: true,
|
||||||
|
// kind: WindowKind::PopUp,
|
||||||
|
// is_movable: false,
|
||||||
|
// screen: Some(screen),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_avatar<T: 'static>(
|
||||||
|
// avatar: Option<Arc<ImageData>>,
|
||||||
|
// avatar_style: &AvatarStyle,
|
||||||
|
// container: ContainerStyle,
|
||||||
|
// ) -> AnyElement<T> {
|
||||||
|
// avatar
|
||||||
|
// .map(|avatar| {
|
||||||
|
// Image::from_data(avatar)
|
||||||
|
// .with_style(avatar_style.image)
|
||||||
|
// .aligned()
|
||||||
|
// .contained()
|
||||||
|
// .with_corner_radius(avatar_style.outer_corner_radius)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(avatar_style.outer_width)
|
||||||
|
// .with_height(avatar_style.outer_width)
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// .unwrap_or_else(|| {
|
||||||
|
// Empty::new()
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(avatar_style.outer_width)
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// .contained()
|
||||||
|
// .with_style(container)
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||||
|
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||||
|
// }
|
113
crates/collab_ui2/src/face_pile.rs
Normal file
113
crates/collab_ui2/src/face_pile.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// use std::ops::Range;
|
||||||
|
|
||||||
|
// use gpui::{
|
||||||
|
// geometry::{
|
||||||
|
// rect::RectF,
|
||||||
|
// vector::{vec2f, Vector2F},
|
||||||
|
// },
|
||||||
|
// json::ToJson,
|
||||||
|
// serde_json::{self, json},
|
||||||
|
// AnyElement, Axis, Element, View, ViewContext,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// pub(crate) struct FacePile<V: View> {
|
||||||
|
// overlap: f32,
|
||||||
|
// faces: Vec<AnyElement<V>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<V: View> FacePile<V> {
|
||||||
|
// pub fn new(overlap: f32) -> Self {
|
||||||
|
// Self {
|
||||||
|
// overlap,
|
||||||
|
// faces: Vec::new(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<V: View> Element<V> for FacePile<V> {
|
||||||
|
// type LayoutState = ();
|
||||||
|
// type PaintState = ();
|
||||||
|
|
||||||
|
// fn layout(
|
||||||
|
// &mut self,
|
||||||
|
// constraint: gpui::SizeConstraint,
|
||||||
|
// view: &mut V,
|
||||||
|
// cx: &mut ViewContext<V>,
|
||||||
|
// ) -> (Vector2F, Self::LayoutState) {
|
||||||
|
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||||
|
|
||||||
|
// let mut width = 0.;
|
||||||
|
// let mut max_height = 0.;
|
||||||
|
// for face in &mut self.faces {
|
||||||
|
// let layout = face.layout(constraint, view, cx);
|
||||||
|
// width += layout.x();
|
||||||
|
// max_height = f32::max(max_height, layout.y());
|
||||||
|
// }
|
||||||
|
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||||
|
|
||||||
|
// (
|
||||||
|
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||||
|
// (),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn paint(
|
||||||
|
// &mut self,
|
||||||
|
// bounds: RectF,
|
||||||
|
// visible_bounds: RectF,
|
||||||
|
// _layout: &mut Self::LayoutState,
|
||||||
|
// view: &mut V,
|
||||||
|
// cx: &mut ViewContext<V>,
|
||||||
|
// ) -> Self::PaintState {
|
||||||
|
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
// let origin_y = bounds.upper_right().y();
|
||||||
|
// let mut origin_x = bounds.upper_right().x();
|
||||||
|
|
||||||
|
// for face in self.faces.iter_mut().rev() {
|
||||||
|
// let size = face.size();
|
||||||
|
// origin_x -= size.x();
|
||||||
|
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||||
|
|
||||||
|
// cx.scene().push_layer(None);
|
||||||
|
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||||
|
// cx.scene().pop_layer();
|
||||||
|
// origin_x += self.overlap;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn rect_for_text_range(
|
||||||
|
// &self,
|
||||||
|
// _: Range<usize>,
|
||||||
|
// _: RectF,
|
||||||
|
// _: RectF,
|
||||||
|
// _: &Self::LayoutState,
|
||||||
|
// _: &Self::PaintState,
|
||||||
|
// _: &V,
|
||||||
|
// _: &ViewContext<V>,
|
||||||
|
// ) -> Option<RectF> {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn debug(
|
||||||
|
// &self,
|
||||||
|
// bounds: RectF,
|
||||||
|
// _: &Self::LayoutState,
|
||||||
|
// _: &Self::PaintState,
|
||||||
|
// _: &V,
|
||||||
|
// _: &ViewContext<V>,
|
||||||
|
// ) -> serde_json::Value {
|
||||||
|
// json!({
|
||||||
|
// "type": "FacePile",
|
||||||
|
// "bounds": bounds.to_json()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
||||||
|
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||||
|
// self.faces.extend(children);
|
||||||
|
// }
|
||||||
|
// }
|
884
crates/collab_ui2/src/notification_panel.rs
Normal file
884
crates/collab_ui2/src/notification_panel.rs
Normal file
|
@ -0,0 +1,884 @@
|
||||||
|
// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
|
||||||
|
// use anyhow::Result;
|
||||||
|
// use channel::ChannelStore;
|
||||||
|
// use client::{Client, Notification, User, UserStore};
|
||||||
|
// use collections::HashMap;
|
||||||
|
// use db::kvp::KEY_VALUE_STORE;
|
||||||
|
// use futures::StreamExt;
|
||||||
|
// use gpui::{
|
||||||
|
// actions,
|
||||||
|
// elements::*,
|
||||||
|
// platform::{CursorStyle, MouseButton},
|
||||||
|
// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
|
||||||
|
// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||||
|
// };
|
||||||
|
// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
|
||||||
|
// use project::Fs;
|
||||||
|
// use rpc::proto;
|
||||||
|
// use serde::{Deserialize, Serialize};
|
||||||
|
// use settings::SettingsStore;
|
||||||
|
// use std::{sync::Arc, time::Duration};
|
||||||
|
// use theme::{ui, Theme};
|
||||||
|
// use time::{OffsetDateTime, UtcOffset};
|
||||||
|
// use util::{ResultExt, TryFutureExt};
|
||||||
|
// use workspace::{
|
||||||
|
// dock::{DockPosition, Panel},
|
||||||
|
// Workspace,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const LOADING_THRESHOLD: usize = 30;
|
||||||
|
// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
|
||||||
|
// const TOAST_DURATION: Duration = Duration::from_secs(5);
|
||||||
|
// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
|
||||||
|
|
||||||
|
// pub struct NotificationPanel {
|
||||||
|
// client: Arc<Client>,
|
||||||
|
// user_store: ModelHandle<UserStore>,
|
||||||
|
// channel_store: ModelHandle<ChannelStore>,
|
||||||
|
// notification_store: ModelHandle<NotificationStore>,
|
||||||
|
// fs: Arc<dyn Fs>,
|
||||||
|
// width: Option<f32>,
|
||||||
|
// active: bool,
|
||||||
|
// notification_list: ListState<Self>,
|
||||||
|
// pending_serialization: Task<Option<()>>,
|
||||||
|
// subscriptions: Vec<gpui::Subscription>,
|
||||||
|
// workspace: WeakViewHandle<Workspace>,
|
||||||
|
// current_notification_toast: Option<(u64, Task<()>)>,
|
||||||
|
// local_timezone: UtcOffset,
|
||||||
|
// has_focus: bool,
|
||||||
|
// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Serialize, Deserialize)]
|
||||||
|
// struct SerializedNotificationPanel {
|
||||||
|
// width: Option<f32>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// pub enum Event {
|
||||||
|
// DockPositionChanged,
|
||||||
|
// Focus,
|
||||||
|
// Dismissed,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct NotificationPresenter {
|
||||||
|
// pub actor: Option<Arc<client::User>>,
|
||||||
|
// pub text: String,
|
||||||
|
// pub icon: &'static str,
|
||||||
|
// pub needs_response: bool,
|
||||||
|
// pub can_navigate: bool,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// actions!(notification_panel, [ToggleFocus]);
|
||||||
|
|
||||||
|
// pub fn init(_cx: &mut AppContext) {}
|
||||||
|
|
||||||
|
// impl NotificationPanel {
|
||||||
|
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||||
|
// let fs = workspace.app_state().fs.clone();
|
||||||
|
// let client = workspace.app_state().client.clone();
|
||||||
|
// let user_store = workspace.app_state().user_store.clone();
|
||||||
|
// let workspace_handle = workspace.weak_handle();
|
||||||
|
|
||||||
|
// cx.add_view(|cx| {
|
||||||
|
// let mut status = client.status();
|
||||||
|
// cx.spawn(|this, mut cx| async move {
|
||||||
|
// while let Some(_) = status.next().await {
|
||||||
|
// if this
|
||||||
|
// .update(&mut cx, |_, cx| {
|
||||||
|
// cx.notify();
|
||||||
|
// })
|
||||||
|
// .is_err()
|
||||||
|
// {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .detach();
|
||||||
|
|
||||||
|
// let mut notification_list =
|
||||||
|
// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
|
||||||
|
// this.render_notification(ix, cx)
|
||||||
|
// .unwrap_or_else(|| Empty::new().into_any())
|
||||||
|
// });
|
||||||
|
// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
|
||||||
|
// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
|
||||||
|
// if let Some(task) = this
|
||||||
|
// .notification_store
|
||||||
|
// .update(cx, |store, cx| store.load_more_notifications(false, cx))
|
||||||
|
// {
|
||||||
|
// task.detach();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// let mut this = Self {
|
||||||
|
// fs,
|
||||||
|
// client,
|
||||||
|
// user_store,
|
||||||
|
// local_timezone: cx.platform().local_timezone(),
|
||||||
|
// channel_store: ChannelStore::global(cx),
|
||||||
|
// notification_store: NotificationStore::global(cx),
|
||||||
|
// notification_list,
|
||||||
|
// pending_serialization: Task::ready(None),
|
||||||
|
// workspace: workspace_handle,
|
||||||
|
// has_focus: false,
|
||||||
|
// current_notification_toast: None,
|
||||||
|
// subscriptions: Vec::new(),
|
||||||
|
// active: false,
|
||||||
|
// mark_as_read_tasks: HashMap::default(),
|
||||||
|
// width: None,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let mut old_dock_position = this.position(cx);
|
||||||
|
// this.subscriptions.extend([
|
||||||
|
// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
|
||||||
|
// cx.subscribe(&this.notification_store, Self::on_notification_event),
|
||||||
|
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
|
||||||
|
// let new_dock_position = this.position(cx);
|
||||||
|
// if new_dock_position != old_dock_position {
|
||||||
|
// old_dock_position = new_dock_position;
|
||||||
|
// cx.emit(Event::DockPositionChanged);
|
||||||
|
// }
|
||||||
|
// cx.notify();
|
||||||
|
// }),
|
||||||
|
// ]);
|
||||||
|
// this
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn load(
|
||||||
|
// workspace: WeakViewHandle<Workspace>,
|
||||||
|
// cx: AsyncAppContext,
|
||||||
|
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||||
|
// cx.spawn(|mut cx| async move {
|
||||||
|
// let serialized_panel = if let Some(panel) = cx
|
||||||
|
// .background()
|
||||||
|
// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
|
||||||
|
// .await
|
||||||
|
// .log_err()
|
||||||
|
// .flatten()
|
||||||
|
// {
|
||||||
|
// Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// };
|
||||||
|
|
||||||
|
// workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
// let panel = Self::new(workspace, cx);
|
||||||
|
// if let Some(serialized_panel) = serialized_panel {
|
||||||
|
// panel.update(cx, |panel, cx| {
|
||||||
|
// panel.width = serialized_panel.width;
|
||||||
|
// cx.notify();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// panel
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
// let width = self.width;
|
||||||
|
// self.pending_serialization = cx.background().spawn(
|
||||||
|
// async move {
|
||||||
|
// KEY_VALUE_STORE
|
||||||
|
// .write_kvp(
|
||||||
|
// NOTIFICATION_PANEL_KEY.into(),
|
||||||
|
// serde_json::to_string(&SerializedNotificationPanel { width })?,
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
// anyhow::Ok(())
|
||||||
|
// }
|
||||||
|
// .log_err(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_notification(
|
||||||
|
// &mut self,
|
||||||
|
// ix: usize,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) -> Option<AnyElement<Self>> {
|
||||||
|
// let entry = self.notification_store.read(cx).notification_at(ix)?;
|
||||||
|
// let notification_id = entry.id;
|
||||||
|
// let now = OffsetDateTime::now_utc();
|
||||||
|
// let timestamp = entry.timestamp;
|
||||||
|
// let NotificationPresenter {
|
||||||
|
// actor,
|
||||||
|
// text,
|
||||||
|
// needs_response,
|
||||||
|
// can_navigate,
|
||||||
|
// ..
|
||||||
|
// } = self.present_notification(entry, cx)?;
|
||||||
|
|
||||||
|
// let theme = theme::current(cx);
|
||||||
|
// let style = &theme.notification_panel;
|
||||||
|
// let response = entry.response;
|
||||||
|
// let notification = entry.notification.clone();
|
||||||
|
|
||||||
|
// let message_style = if entry.is_read {
|
||||||
|
// style.read_text.clone()
|
||||||
|
// } else {
|
||||||
|
// style.unread_text.clone()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if self.active && !entry.is_read {
|
||||||
|
// self.did_render_notification(notification_id, ¬ification, cx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// enum Decline {}
|
||||||
|
// enum Accept {}
|
||||||
|
|
||||||
|
// Some(
|
||||||
|
// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
|
||||||
|
// let container = message_style.container;
|
||||||
|
|
||||||
|
// Flex::row()
|
||||||
|
// .with_children(actor.map(|actor| {
|
||||||
|
// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
|
||||||
|
// }))
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(Text::new(text, message_style.text.clone()))
|
||||||
|
// .with_child(
|
||||||
|
// Flex::row()
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// format_timestamp(timestamp, now, self.local_timezone),
|
||||||
|
// style.timestamp.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.timestamp.container),
|
||||||
|
// )
|
||||||
|
// .with_children(if let Some(is_accepted) = response {
|
||||||
|
// Some(
|
||||||
|
// Label::new(
|
||||||
|
// if is_accepted {
|
||||||
|
// "You accepted"
|
||||||
|
// } else {
|
||||||
|
// "You declined"
|
||||||
|
// },
|
||||||
|
// style.read_text.text.clone(),
|
||||||
|
// )
|
||||||
|
// .flex_float()
|
||||||
|
// .into_any(),
|
||||||
|
// )
|
||||||
|
// } else if needs_response {
|
||||||
|
// Some(
|
||||||
|
// Flex::row()
|
||||||
|
// .with_children([
|
||||||
|
// MouseEventHandler::new::<Decline, _>(
|
||||||
|
// ix,
|
||||||
|
// cx,
|
||||||
|
// |state, _| {
|
||||||
|
// let button =
|
||||||
|
// style.button.style_for(state);
|
||||||
|
// Label::new(
|
||||||
|
// "Decline",
|
||||||
|
// button.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(button.container)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .on_click(MouseButton::Left, {
|
||||||
|
// let notification = notification.clone();
|
||||||
|
// move |_, view, cx| {
|
||||||
|
// view.respond_to_notification(
|
||||||
|
// notification.clone(),
|
||||||
|
// false,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// MouseEventHandler::new::<Accept, _>(
|
||||||
|
// ix,
|
||||||
|
// cx,
|
||||||
|
// |state, _| {
|
||||||
|
// let button =
|
||||||
|
// style.button.style_for(state);
|
||||||
|
// Label::new(
|
||||||
|
// "Accept",
|
||||||
|
// button.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(button.container)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .on_click(MouseButton::Left, {
|
||||||
|
// let notification = notification.clone();
|
||||||
|
// move |_, view, cx| {
|
||||||
|
// view.respond_to_notification(
|
||||||
|
// notification.clone(),
|
||||||
|
// true,
|
||||||
|
// cx,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// ])
|
||||||
|
// .flex_float()
|
||||||
|
// .into_any(),
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// .flex(1.0, true),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(container)
|
||||||
|
// .into_any()
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(if can_navigate {
|
||||||
|
// CursorStyle::PointingHand
|
||||||
|
// } else {
|
||||||
|
// CursorStyle::default()
|
||||||
|
// })
|
||||||
|
// .on_click(MouseButton::Left, {
|
||||||
|
// let notification = notification.clone();
|
||||||
|
// move |_, this, cx| this.did_click_notification(¬ification, cx)
|
||||||
|
// })
|
||||||
|
// .into_any(),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn present_notification(
|
||||||
|
// &self,
|
||||||
|
// entry: &NotificationEntry,
|
||||||
|
// cx: &AppContext,
|
||||||
|
// ) -> Option<NotificationPresenter> {
|
||||||
|
// let user_store = self.user_store.read(cx);
|
||||||
|
// let channel_store = self.channel_store.read(cx);
|
||||||
|
// match entry.notification {
|
||||||
|
// Notification::ContactRequest { sender_id } => {
|
||||||
|
// let requester = user_store.get_cached_user(sender_id)?;
|
||||||
|
// Some(NotificationPresenter {
|
||||||
|
// icon: "icons/plus.svg",
|
||||||
|
// text: format!("{} wants to add you as a contact", requester.github_login),
|
||||||
|
// needs_response: user_store.has_incoming_contact_request(requester.id),
|
||||||
|
// actor: Some(requester),
|
||||||
|
// can_navigate: false,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// Notification::ContactRequestAccepted { responder_id } => {
|
||||||
|
// let responder = user_store.get_cached_user(responder_id)?;
|
||||||
|
// Some(NotificationPresenter {
|
||||||
|
// icon: "icons/plus.svg",
|
||||||
|
// text: format!("{} accepted your contact invite", responder.github_login),
|
||||||
|
// needs_response: false,
|
||||||
|
// actor: Some(responder),
|
||||||
|
// can_navigate: false,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// Notification::ChannelInvitation {
|
||||||
|
// ref channel_name,
|
||||||
|
// channel_id,
|
||||||
|
// inviter_id,
|
||||||
|
// } => {
|
||||||
|
// let inviter = user_store.get_cached_user(inviter_id)?;
|
||||||
|
// Some(NotificationPresenter {
|
||||||
|
// icon: "icons/hash.svg",
|
||||||
|
// text: format!(
|
||||||
|
// "{} invited you to join the #{channel_name} channel",
|
||||||
|
// inviter.github_login
|
||||||
|
// ),
|
||||||
|
// needs_response: channel_store.has_channel_invitation(channel_id),
|
||||||
|
// actor: Some(inviter),
|
||||||
|
// can_navigate: false,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// Notification::ChannelMessageMention {
|
||||||
|
// sender_id,
|
||||||
|
// channel_id,
|
||||||
|
// message_id,
|
||||||
|
// } => {
|
||||||
|
// let sender = user_store.get_cached_user(sender_id)?;
|
||||||
|
// let channel = channel_store.channel_for_id(channel_id)?;
|
||||||
|
// let message = self
|
||||||
|
// .notification_store
|
||||||
|
// .read(cx)
|
||||||
|
// .channel_message_for_id(message_id)?;
|
||||||
|
// Some(NotificationPresenter {
|
||||||
|
// icon: "icons/conversations.svg",
|
||||||
|
// text: format!(
|
||||||
|
// "{} mentioned you in #{}:\n{}",
|
||||||
|
// sender.github_login, channel.name, message.body,
|
||||||
|
// ),
|
||||||
|
// needs_response: false,
|
||||||
|
// actor: Some(sender),
|
||||||
|
// can_navigate: true,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn did_render_notification(
|
||||||
|
// &mut self,
|
||||||
|
// notification_id: u64,
|
||||||
|
// notification: &Notification,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) {
|
||||||
|
// let should_mark_as_read = match notification {
|
||||||
|
// Notification::ContactRequestAccepted { .. } => true,
|
||||||
|
// Notification::ContactRequest { .. }
|
||||||
|
// | Notification::ChannelInvitation { .. }
|
||||||
|
// | Notification::ChannelMessageMention { .. } => false,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if should_mark_as_read {
|
||||||
|
// self.mark_as_read_tasks
|
||||||
|
// .entry(notification_id)
|
||||||
|
// .or_insert_with(|| {
|
||||||
|
// let client = self.client.clone();
|
||||||
|
// cx.spawn(|this, mut cx| async move {
|
||||||
|
// cx.background().timer(MARK_AS_READ_DELAY).await;
|
||||||
|
// client
|
||||||
|
// .request(proto::MarkNotificationRead { notification_id })
|
||||||
|
// .await?;
|
||||||
|
// this.update(&mut cx, |this, _| {
|
||||||
|
// this.mark_as_read_tasks.remove(¬ification_id);
|
||||||
|
// })?;
|
||||||
|
// Ok(())
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Notification::ChannelMessageMention {
|
||||||
|
// message_id,
|
||||||
|
// channel_id,
|
||||||
|
// ..
|
||||||
|
// } = notification.clone()
|
||||||
|
// {
|
||||||
|
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
// cx.app_context().defer(move |cx| {
|
||||||
|
// workspace.update(cx, |workspace, cx| {
|
||||||
|
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
|
||||||
|
// panel.update(cx, |panel, cx| {
|
||||||
|
// panel
|
||||||
|
// .select_channel(channel_id, Some(message_id), cx)
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
|
||||||
|
// if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
|
||||||
|
// if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
|
// return workspace
|
||||||
|
// .read_with(cx, |workspace, cx| {
|
||||||
|
// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
|
||||||
|
// return panel.read_with(cx, |panel, cx| {
|
||||||
|
// panel.is_scrolled_to_bottom()
|
||||||
|
// && panel.active_chat().map_or(false, |chat| {
|
||||||
|
// chat.read(cx).channel_id == *channel_id
|
||||||
|
// })
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// false
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 view your notifications".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(|_, cx| async move {
|
||||||
|
// client.authenticate_and_connect(true, &cx).log_err().await;
|
||||||
|
// })
|
||||||
|
// .detach();
|
||||||
|
// })
|
||||||
|
// .aligned()
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render_empty_state(
|
||||||
|
// &self,
|
||||||
|
// theme: &Arc<Theme>,
|
||||||
|
// _cx: &mut ViewContext<Self>,
|
||||||
|
// ) -> AnyElement<Self> {
|
||||||
|
// Label::new(
|
||||||
|
// "You have no notifications".to_string(),
|
||||||
|
// theme.chat_panel.sign_in_prompt.default.clone(),
|
||||||
|
// )
|
||||||
|
// .aligned()
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn on_notification_event(
|
||||||
|
// &mut self,
|
||||||
|
// _: ModelHandle<NotificationStore>,
|
||||||
|
// event: &NotificationEvent,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) {
|
||||||
|
// match event {
|
||||||
|
// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
|
||||||
|
// NotificationEvent::NotificationRemoved { entry }
|
||||||
|
// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
|
||||||
|
// NotificationEvent::NotificationsUpdated {
|
||||||
|
// old_range,
|
||||||
|
// new_count,
|
||||||
|
// } => {
|
||||||
|
// self.notification_list.splice(old_range.clone(), *new_count);
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
|
||||||
|
// if self.is_showing_notification(&entry.notification, cx) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
|
||||||
|
// else {
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let notification_id = entry.id;
|
||||||
|
// self.current_notification_toast = Some((
|
||||||
|
// notification_id,
|
||||||
|
// cx.spawn(|this, mut cx| async move {
|
||||||
|
// cx.background().timer(TOAST_DURATION).await;
|
||||||
|
// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
|
||||||
|
// .ok();
|
||||||
|
// }),
|
||||||
|
// ));
|
||||||
|
|
||||||
|
// self.workspace
|
||||||
|
// .update(cx, |workspace, cx| {
|
||||||
|
// workspace.dismiss_notification::<NotificationToast>(0, cx);
|
||||||
|
// workspace.show_notification(0, cx, |cx| {
|
||||||
|
// let workspace = cx.weak_handle();
|
||||||
|
// cx.add_view(|_| NotificationToast {
|
||||||
|
// notification_id,
|
||||||
|
// actor,
|
||||||
|
// text,
|
||||||
|
// workspace,
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// .ok();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
|
||||||
|
// if let Some((current_id, _)) = &self.current_notification_toast {
|
||||||
|
// if *current_id == notification_id {
|
||||||
|
// self.current_notification_toast.take();
|
||||||
|
// self.workspace
|
||||||
|
// .update(cx, |workspace, cx| {
|
||||||
|
// workspace.dismiss_notification::<NotificationToast>(0, cx)
|
||||||
|
// })
|
||||||
|
// .ok();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn respond_to_notification(
|
||||||
|
// &mut self,
|
||||||
|
// notification: Notification,
|
||||||
|
// response: bool,
|
||||||
|
// cx: &mut ViewContext<Self>,
|
||||||
|
// ) {
|
||||||
|
// self.notification_store.update(cx, |store, cx| {
|
||||||
|
// store.respond_to_notification(notification, response, cx);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Entity for NotificationPanel {
|
||||||
|
// type Event = Event;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl View for NotificationPanel {
|
||||||
|
// fn ui_name() -> &'static str {
|
||||||
|
// "NotificationPanel"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// let theme = theme::current(cx);
|
||||||
|
// let style = &theme.notification_panel;
|
||||||
|
// let element = if self.client.user_id().is_none() {
|
||||||
|
// self.render_sign_in_prompt(&theme, cx)
|
||||||
|
// } else if self.notification_list.item_count() == 0 {
|
||||||
|
// self.render_empty_state(&theme, cx)
|
||||||
|
// } else {
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Flex::row()
|
||||||
|
// .with_child(Label::new("Notifications", style.title.text.clone()))
|
||||||
|
// .with_child(ui::svg(&style.title_icon).flex_float())
|
||||||
|
// .align_children_center()
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.title.container)
|
||||||
|
// .constrained()
|
||||||
|
// .with_height(style.title_height),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// List::new(self.notification_list.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.list)
|
||||||
|
// .flex(1., true),
|
||||||
|
// )
|
||||||
|
// .into_any()
|
||||||
|
// };
|
||||||
|
// element
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container)
|
||||||
|
// .constrained()
|
||||||
|
// .with_min_width(150.)
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
// self.has_focus = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||||
|
// self.has_focus = false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Panel for NotificationPanel {
|
||||||
|
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
|
||||||
|
// settings::get::<NotificationPanelSettings>(cx).dock
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||||
|
// matches!(position, DockPosition::Left | DockPosition::Right)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||||
|
// settings::update_settings_file::<NotificationPanelSettings>(
|
||||||
|
// self.fs.clone(),
|
||||||
|
// cx,
|
||||||
|
// move |settings| settings.dock = Some(position),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
|
||||||
|
// self.width
|
||||||
|
// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.width = size;
|
||||||
|
// self.serialize(cx);
|
||||||
|
// cx.notify();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
// self.active = active;
|
||||||
|
// if self.notification_store.read(cx).notification_count() == 0 {
|
||||||
|
// cx.emit(Event::Dismissed);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
|
||||||
|
// (settings::get::<NotificationPanelSettings>(cx).button
|
||||||
|
// && self.notification_store.read(cx).notification_count() > 0)
|
||||||
|
// .then(|| "icons/bell.svg")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
|
||||||
|
// (
|
||||||
|
// "Notification Panel".to_string(),
|
||||||
|
// Some(Box::new(ToggleFocus)),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||||
|
// let count = self.notification_store.read(cx).unread_notification_count();
|
||||||
|
// if count == 0 {
|
||||||
|
// None
|
||||||
|
// } else {
|
||||||
|
// Some(count.to_string())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct NotificationToast {
|
||||||
|
// notification_id: u64,
|
||||||
|
// actor: Option<Arc<User>>,
|
||||||
|
// text: String,
|
||||||
|
// workspace: WeakViewHandle<Workspace>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub enum ToastEvent {
|
||||||
|
// Dismiss,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl NotificationToast {
|
||||||
|
// fn focus_notification_panel(&self, cx: &mut AppContext) {
|
||||||
|
// let workspace = self.workspace.clone();
|
||||||
|
// let notification_id = self.notification_id;
|
||||||
|
// cx.defer(move |cx| {
|
||||||
|
// workspace
|
||||||
|
// .update(cx, |workspace, cx| {
|
||||||
|
// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
|
||||||
|
// panel.update(cx, |panel, cx| {
|
||||||
|
// let store = panel.notification_store.read(cx);
|
||||||
|
// if let Some(entry) = store.notification_for_id(notification_id) {
|
||||||
|
// panel.did_click_notification(&entry.clone().notification, cx);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .ok();
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Entity for NotificationToast {
|
||||||
|
// type Event = ToastEvent;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl View for NotificationToast {
|
||||||
|
// fn ui_name() -> &'static str {
|
||||||
|
// "ContactNotification"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
// let user = self.actor.clone();
|
||||||
|
// let theme = theme::current(cx).clone();
|
||||||
|
// let theme = &theme.contact_notification;
|
||||||
|
|
||||||
|
// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
|
||||||
|
// Flex::row()
|
||||||
|
// .with_children(user.and_then(|user| {
|
||||||
|
// Some(
|
||||||
|
// Image::from_data(user.avatar.clone()?)
|
||||||
|
// .with_style(theme.header_avatar)
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_height(
|
||||||
|
// cx.font_cache()
|
||||||
|
// .line_height(theme.header_message.text.font_size),
|
||||||
|
// )
|
||||||
|
// .aligned()
|
||||||
|
// .top(),
|
||||||
|
// )
|
||||||
|
// }))
|
||||||
|
// .with_child(
|
||||||
|
// Text::new(self.text.clone(), theme.header_message.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.header_message.container)
|
||||||
|
// .aligned()
|
||||||
|
// .top()
|
||||||
|
// .left()
|
||||||
|
// .flex(1., true),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
|
||||||
|
// let style = theme.dismiss_button.style_for(state);
|
||||||
|
// Svg::new("icons/x.svg")
|
||||||
|
// .with_color(style.color)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.icon_width)
|
||||||
|
// .aligned()
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(style.button_width)
|
||||||
|
// .with_height(style.button_width)
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .with_padding(Padding::uniform(5.))
|
||||||
|
// .on_click(MouseButton::Left, move |_, _, cx| {
|
||||||
|
// cx.emit(ToastEvent::Dismiss)
|
||||||
|
// })
|
||||||
|
// .aligned()
|
||||||
|
// .constrained()
|
||||||
|
// .with_height(
|
||||||
|
// cx.font_cache()
|
||||||
|
// .line_height(theme.header_message.text.font_size),
|
||||||
|
// )
|
||||||
|
// .aligned()
|
||||||
|
// .top()
|
||||||
|
// .flex_float(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
// this.focus_notification_panel(cx);
|
||||||
|
// cx.emit(ToastEvent::Dismiss);
|
||||||
|
// })
|
||||||
|
// .into_any()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl workspace::notifications::Notification for NotificationToast {
|
||||||
|
// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
||||||
|
// matches!(event, ToastEvent::Dismiss)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn format_timestamp(
|
||||||
|
// mut timestamp: OffsetDateTime,
|
||||||
|
// mut now: OffsetDateTime,
|
||||||
|
// local_timezone: UtcOffset,
|
||||||
|
// ) -> String {
|
||||||
|
// timestamp = timestamp.to_offset(local_timezone);
|
||||||
|
// now = now.to_offset(local_timezone);
|
||||||
|
|
||||||
|
// let today = now.date();
|
||||||
|
// let date = timestamp.date();
|
||||||
|
// if date == today {
|
||||||
|
// let difference = now - timestamp;
|
||||||
|
// if difference >= Duration::from_secs(3600) {
|
||||||
|
// format!("{}h", difference.whole_seconds() / 3600)
|
||||||
|
// } else if difference >= Duration::from_secs(60) {
|
||||||
|
// format!("{}m", difference.whole_seconds() / 60)
|
||||||
|
// } else {
|
||||||
|
// "just now".to_string()
|
||||||
|
// }
|
||||||
|
// } else if date.next_day() == Some(today) {
|
||||||
|
// format!("yesterday")
|
||||||
|
// } else {
|
||||||
|
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||||
|
// }
|
||||||
|
// }
|
11
crates/collab_ui2/src/notifications.rs
Normal file
11
crates/collab_ui2/src/notifications.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// use gpui::AppContext;
|
||||||
|
// use std::sync::Arc;
|
||||||
|
// use workspace::AppState;
|
||||||
|
|
||||||
|
// pub mod incoming_call_notification;
|
||||||
|
// pub mod project_shared_notification;
|
||||||
|
|
||||||
|
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
// incoming_call_notification::init(app_state, cx);
|
||||||
|
// project_shared_notification::init(app_state, cx);
|
||||||
|
// }
|
|
@ -0,0 +1,213 @@
|
||||||
|
use crate::notification_window_options;
|
||||||
|
use call::{ActiveCall, IncomingCall};
|
||||||
|
use client::proto;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::{
|
||||||
|
elements::*,
|
||||||
|
geometry::vector::vec2f,
|
||||||
|
platform::{CursorStyle, MouseButton},
|
||||||
|
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::AppState;
|
||||||
|
|
||||||
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
let app_state = Arc::downgrade(app_state);
|
||||||
|
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
|
||||||
|
while let Some(incoming_call) = incoming_call.next().await {
|
||||||
|
for window in notification_windows.drain(..) {
|
||||||
|
window.remove(&mut cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(incoming_call) = incoming_call {
|
||||||
|
let window_size = cx.read(|cx| {
|
||||||
|
let theme = &theme::current(cx).incoming_call_notification;
|
||||||
|
vec2f(theme.window_width, theme.window_height)
|
||||||
|
});
|
||||||
|
|
||||||
|
for screen in cx.platform().screens() {
|
||||||
|
let window = cx
|
||||||
|
.add_window(notification_window_options(screen, window_size), |_| {
|
||||||
|
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
notification_windows.push(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct RespondToCall {
|
||||||
|
accept: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IncomingCallNotification {
|
||||||
|
call: IncomingCall,
|
||||||
|
app_state: Weak<AppState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncomingCallNotification {
|
||||||
|
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||||
|
Self { call, app_state }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
if accept {
|
||||||
|
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||||
|
let caller_user_id = self.call.calling_user.id;
|
||||||
|
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||||
|
let app_state = self.app_state.clone();
|
||||||
|
cx.app_context()
|
||||||
|
.spawn(|mut cx| async move {
|
||||||
|
join.await?;
|
||||||
|
if let Some(project_id) = initial_project_id {
|
||||||
|
cx.update(|cx| {
|
||||||
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
|
workspace::join_remote_project(
|
||||||
|
project_id,
|
||||||
|
caller_user_id,
|
||||||
|
app_state,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
} else {
|
||||||
|
active_call.update(cx, |active_call, cx| {
|
||||||
|
active_call.decline_incoming(cx).log_err();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
let theme = &theme::current(cx).incoming_call_notification;
|
||||||
|
let default_project = proto::ParticipantProject::default();
|
||||||
|
let initial_project = self
|
||||||
|
.call
|
||||||
|
.initial_project
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&default_project);
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||||
|
Image::from_data(avatar)
|
||||||
|
.with_style(theme.caller_avatar)
|
||||||
|
.aligned()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.call.calling_user.github_login.clone(),
|
||||||
|
theme.caller_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_username.container),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if initial_project.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.caller_message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_message.container),
|
||||||
|
)
|
||||||
|
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
initial_project.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_metadata)
|
||||||
|
.aligned(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.caller_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
enum Accept {}
|
||||||
|
enum Decline {}
|
||||||
|
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||||
|
let theme = &theme.incoming_call_notification;
|
||||||
|
Label::new("Accept", theme.accept_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.accept_button.container)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, this, cx| {
|
||||||
|
this.respond(true, cx);
|
||||||
|
})
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||||
|
let theme = &theme.incoming_call_notification;
|
||||||
|
Label::new("Decline", theme.decline_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.decline_button.container)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, this, cx| {
|
||||||
|
this.respond(false, cx);
|
||||||
|
})
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.incoming_call_notification.button_width)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for IncomingCallNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for IncomingCallNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"IncomingCallNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
let background = theme::current(cx).incoming_call_notification.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_caller(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
use crate::notification_window_options;
|
||||||
|
use call::{room, ActiveCall};
|
||||||
|
use client::User;
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::{
|
||||||
|
elements::*,
|
||||||
|
geometry::vector::vec2f,
|
||||||
|
platform::{CursorStyle, MouseButton},
|
||||||
|
AppContext, Entity, View, ViewContext,
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use workspace::AppState;
|
||||||
|
|
||||||
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
let app_state = Arc::downgrade(app_state);
|
||||||
|
let active_call = ActiveCall::global(cx);
|
||||||
|
let mut notification_windows = HashMap::default();
|
||||||
|
cx.subscribe(&active_call, move |_, event, cx| match event {
|
||||||
|
room::Event::RemoteProjectShared {
|
||||||
|
owner,
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
} => {
|
||||||
|
let theme = &theme::current(cx).project_shared_notification;
|
||||||
|
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||||
|
|
||||||
|
for screen in cx.platform().screens() {
|
||||||
|
let window =
|
||||||
|
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||||
|
ProjectSharedNotification::new(
|
||||||
|
owner.clone(),
|
||||||
|
*project_id,
|
||||||
|
worktree_root_names.clone(),
|
||||||
|
app_state.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
notification_windows
|
||||||
|
.entry(*project_id)
|
||||||
|
.or_insert(Vec::new())
|
||||||
|
.push(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room::Event::RemoteProjectUnshared { project_id }
|
||||||
|
| room::Event::RemoteProjectJoined { project_id }
|
||||||
|
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
|
||||||
|
if let Some(windows) = notification_windows.remove(&project_id) {
|
||||||
|
for window in windows {
|
||||||
|
window.remove(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
room::Event::Left => {
|
||||||
|
for (_, windows) in notification_windows.drain() {
|
||||||
|
for window in windows {
|
||||||
|
window.remove(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectSharedNotification {
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
owner: Arc<User>,
|
||||||
|
app_state: Weak<AppState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSharedNotification {
|
||||||
|
fn new(
|
||||||
|
owner: Arc<User>,
|
||||||
|
project_id: u64,
|
||||||
|
worktree_root_names: Vec<String>,
|
||||||
|
app_state: Weak<AppState>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
project_id,
|
||||||
|
worktree_root_names,
|
||||||
|
owner,
|
||||||
|
app_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(app_state) = self.app_state.upgrade() {
|
||||||
|
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(active_room) =
|
||||||
|
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
|
||||||
|
{
|
||||||
|
active_room.update(cx, |_, cx| {
|
||||||
|
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
|
||||||
|
project_id: self.project_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
let theme = &theme::current(cx).project_shared_notification;
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||||
|
Image::from_data(avatar)
|
||||||
|
.with_style(theme.owner_avatar)
|
||||||
|
.aligned()
|
||||||
|
}))
|
||||||
|
.with_child(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
self.owner.github_login.clone(),
|
||||||
|
theme.owner_username.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_username.container),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"is sharing a project in Zed{}",
|
||||||
|
if self.worktree_root_names.is_empty() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
":"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
theme.message.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.message.container),
|
||||||
|
)
|
||||||
|
.with_children(if self.worktree_root_names.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
Label::new(
|
||||||
|
self.worktree_root_names.join(", "),
|
||||||
|
theme.worktree_roots.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.worktree_roots.container),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_metadata)
|
||||||
|
.aligned(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.owner_container)
|
||||||
|
.flex(1., true)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
|
enum Open {}
|
||||||
|
enum Dismiss {}
|
||||||
|
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||||
|
let theme = &theme.project_shared_notification;
|
||||||
|
Label::new("Open", theme.open_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.open_button.container)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||||
|
let theme = &theme.project_shared_notification;
|
||||||
|
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.dismiss_button.container)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, this, cx| {
|
||||||
|
this.dismiss(cx);
|
||||||
|
})
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.project_shared_notification.button_width)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ProjectSharedNotification {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ProjectSharedNotification {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ProjectSharedNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||||
|
let background = theme::current(cx).project_shared_notification.background;
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_owner(cx))
|
||||||
|
.with_child(self.render_buttons(cx))
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.expanded()
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
69
crates/collab_ui2/src/panel_settings.rs
Normal file
69
crates/collab_ui2/src/panel_settings.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use anyhow;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use settings::Settings;
|
||||||
|
use workspace::dock::DockPosition;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct CollaborationPanelSettings {
|
||||||
|
pub button: bool,
|
||||||
|
pub dock: DockPosition,
|
||||||
|
pub default_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ChatPanelSettings {
|
||||||
|
pub button: bool,
|
||||||
|
pub dock: DockPosition,
|
||||||
|
pub default_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct NotificationPanelSettings {
|
||||||
|
pub button: bool,
|
||||||
|
pub dock: DockPosition,
|
||||||
|
pub default_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
pub struct PanelSettingsContent {
|
||||||
|
pub button: Option<bool>,
|
||||||
|
pub dock: Option<DockPosition>,
|
||||||
|
pub default_width: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for CollaborationPanelSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||||
|
type FileContent = PanelSettingsContent;
|
||||||
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for ChatPanelSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("chat_panel");
|
||||||
|
type FileContent = PanelSettingsContent;
|
||||||
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for NotificationPanelSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("notification_panel");
|
||||||
|
type FileContent = PanelSettingsContent;
|
||||||
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Self::load_via_json_merge(default_value, user_values)
|
||||||
|
}
|
||||||
|
}
|
|
@ -385,8 +385,7 @@ mod tests {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
let editor = cx.build_view(|cx| {
|
let editor = cx.build_view(|cx| {
|
||||||
let mut editor = Editor::single_line(cx);
|
let mut editor = Editor::single_line(cx);
|
||||||
|
@ -417,7 +416,7 @@ mod tests {
|
||||||
assert!(is_sorted(&palette.delegate.commands));
|
assert!(is_sorted(&palette.delegate.commands));
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.simulate_keystrokes("b c k s p");
|
cx.simulate_input("bcksp");
|
||||||
|
|
||||||
palette.update(cx, |palette, _| {
|
palette.update(cx, |palette, _| {
|
||||||
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
|
||||||
|
@ -439,7 +438,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.simulate_keystrokes("cmd-shift-p");
|
cx.simulate_keystrokes("cmd-shift-p");
|
||||||
cx.simulate_keystrokes("b c k s p");
|
cx.simulate_input("bcksp");
|
||||||
|
|
||||||
let palette = workspace.update(cx, |workspace, cx| {
|
let palette = workspace.update(cx, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
|
|
|
@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
|
||||||
use theme::{
|
use theme::{
|
||||||
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
|
||||||
};
|
};
|
||||||
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
|
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
|
||||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{ItemEvent, ItemHandle},
|
item::{ItemEvent, ItemHandle},
|
||||||
|
@ -9991,7 +9991,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||||
})
|
})
|
||||||
.tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
|
.tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx))
|
||||||
.render()
|
.render()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
|
|
||||||
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
view.condition::<crate::Event>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.change_selections(None, cx, |s| {
|
view.change_selections(None, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||||
|
@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
|
view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||||
|
@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trying to expand the selected syntax node one more time has no effect.
|
// Trying to expand the selected syntax node one more time has no effect.
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
|
||||||
);
|
);
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||||
|
@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
||||||
|
@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trying to shrink the selected syntax node one more time has no effect.
|
// Trying to shrink the selected syntax node one more time has no effect.
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
|
||||||
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
|
||||||
|
@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
// Ensure that we keep expanding the selection if the larger selection starts or ends within
|
||||||
// a fold.
|
// a fold.
|
||||||
view.update(&mut cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.fold_ranges(
|
view.fold_ranges(
|
||||||
vec![
|
vec![
|
||||||
Point::new(0, 21)..Point::new(0, 24),
|
Point::new(0, 21)..Point::new(0, 24),
|
||||||
|
@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||||
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
|
||||||
});
|
});
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
|
view.update(cx, |view, cx| view.selections.display_ranges(cx)),
|
||||||
&[
|
&[
|
||||||
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
|
||||||
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
|
||||||
|
@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
|
||||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor
|
editor
|
||||||
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
.condition::<crate::Event>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor
|
editor
|
||||||
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||||
|
|
||||||
|
@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
assert!(cx.read(|cx| editor.is_dirty(cx)));
|
||||||
|
|
||||||
|
@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
let fake_server = fake_servers.next().await.unwrap();
|
let fake_server = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||||
|
|
||||||
let format = editor
|
let format = editor
|
||||||
|
@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
assert_eq!(view.text(cx), "aaaa\nbbbb");
|
assert_eq!(view.text(cx), "aaaa\nbbbb");
|
||||||
view.change_selections(None, cx, |s| {
|
view.change_selections(None, cx, |s| {
|
||||||
|
@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
let (expected_text, selection_ranges) = marked_text_ranges(
|
let (expected_text, selection_ranges) = marked_text_ranges(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
||||||
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
|
||||||
});
|
});
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
view.condition::<crate::Event>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
let buffer_text = "one\ntwo\nthree\n";
|
let buffer_text = "one\ntwo\nthree\n";
|
||||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||||
|
|
||||||
editor
|
editor
|
||||||
|
|
|
@ -12,8 +12,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||||
HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, PageDown, PageUp, Point, SelectPhase,
|
HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point,
|
||||||
Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
|
SelectPhase, Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
|
@ -45,7 +45,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use theme::{ActiveTheme, PlayerColor};
|
use theme::{ActiveTheme, PlayerColor};
|
||||||
use ui::{h_stack, IconButton};
|
use ui::{h_stack, IconButton, Tooltip};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::item::Item;
|
use workspace::item::Item;
|
||||||
|
|
||||||
|
@ -2036,7 +2036,9 @@ impl EditorElement {
|
||||||
.on_click(move |editor: &mut Editor, cx| {
|
.on_click(move |editor: &mut Editor, cx| {
|
||||||
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
|
editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
|
||||||
})
|
})
|
||||||
.tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding)
|
.tooltip(move |_, cx| {
|
||||||
|
Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let element = if *starts_new_buffer {
|
let element = if *starts_new_buffer {
|
||||||
|
@ -2398,21 +2400,14 @@ impl Element<Editor> for EditorElement {
|
||||||
Some(self.editor_id.into())
|
Some(self.editor_id.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut gpui::ViewContext<Editor>,
|
cx: &mut gpui::ViewContext<Editor>,
|
||||||
) -> Self::ElementState {
|
) -> (gpui::LayoutId, Self::ElementState) {
|
||||||
editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
|
editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
editor: &mut Editor,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut gpui::ViewContext<Editor>,
|
|
||||||
) -> gpui::LayoutId {
|
|
||||||
let rem_size = cx.rem_size();
|
let rem_size = cx.rem_size();
|
||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
style.size.width = relative(1.).into();
|
style.size.width = relative(1.).into();
|
||||||
|
@ -2421,7 +2416,8 @@ impl Element<Editor> for EditorElement {
|
||||||
EditorMode::AutoHeight { .. } => todo!(),
|
EditorMode::AutoHeight { .. } => todo!(),
|
||||||
EditorMode::Full => relative(1.).into(),
|
EditorMode::Full => relative(1.).into(),
|
||||||
};
|
};
|
||||||
cx.request_layout(&style, None)
|
let layout_id = cx.request_layout(&style, None);
|
||||||
|
(layout_id, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
|
|
@ -775,8 +775,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, workspace, mut cx) = build_find_picker(project, cx);
|
let (picker, workspace, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
cx.simulate_input("bna");
|
cx.simulate_input("bna");
|
||||||
|
|
||||||
|
@ -815,8 +814,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, workspace, mut cx) = build_find_picker(project, cx);
|
let (picker, workspace, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
let file_query = &first_file_name[..3];
|
let file_query = &first_file_name[..3];
|
||||||
let file_row = 1;
|
let file_row = 1;
|
||||||
|
@ -891,8 +889,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, workspace, mut cx) = build_find_picker(project, cx);
|
let (picker, workspace, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
let file_query = &first_file_name[..3];
|
let file_query = &first_file_name[..3];
|
||||||
let file_row = 200;
|
let file_row = 200;
|
||||||
|
@ -967,8 +964,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
|
||||||
let (picker, _, mut cx) = build_find_picker(project, cx);
|
let (picker, _, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
let query = test_path_like("hi");
|
let query = test_path_like("hi");
|
||||||
picker
|
picker
|
||||||
|
@ -1055,8 +1051,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (picker, _, mut cx) = build_find_picker(project, cx);
|
let (picker, _, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
picker
|
picker
|
||||||
.update(cx, |picker, cx| {
|
.update(cx, |picker, cx| {
|
||||||
|
@ -1082,8 +1077,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (picker, _, mut cx) = build_find_picker(project, cx);
|
let (picker, _, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
// Even though there is only one worktree, that worktree's filename
|
// Even though there is only one worktree, that worktree's filename
|
||||||
// is included in the matching, because the worktree is a single file.
|
// is included in the matching, because the worktree is a single file.
|
||||||
|
@ -1139,8 +1133,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
let worktree_id = cx.read(|cx| {
|
let worktree_id = cx.read(|cx| {
|
||||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
|
@ -1198,8 +1191,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (picker, _workspace, mut cx) = build_find_picker(project, cx);
|
let (picker, _workspace, cx) = build_find_picker(project, cx);
|
||||||
let cx = &mut cx;
|
|
||||||
picker
|
picker
|
||||||
.update(cx, |f, cx| {
|
.update(cx, |f, cx| {
|
||||||
f.delegate.spawn_search(test_path_like("dir"), cx)
|
f.delegate.spawn_search(test_path_like("dir"), cx)
|
||||||
|
@ -1231,8 +1224,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
let worktree_id = cx.read(|cx| {
|
let worktree_id = cx.read(|cx| {
|
||||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
assert_eq!(worktrees.len(), 1);
|
assert_eq!(worktrees.len(), 1);
|
||||||
|
@ -1395,8 +1387,7 @@ mod tests {
|
||||||
.detach();
|
.detach();
|
||||||
cx.background_executor.run_until_parked();
|
cx.background_executor.run_until_parked();
|
||||||
|
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
let worktree_id = cx.read(|cx| {
|
let worktree_id = cx.read(|cx| {
|
||||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
assert_eq!(worktrees.len(), 1,);
|
assert_eq!(worktrees.len(), 1,);
|
||||||
|
@ -1488,8 +1479,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
|
|
||||||
// generate some history to select from
|
// generate some history to select from
|
||||||
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
||||||
|
@ -1547,8 +1537,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
let worktree_id = cx.read(|cx| {
|
let worktree_id = cx.read(|cx| {
|
||||||
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
|
||||||
assert_eq!(worktrees.len(), 1,);
|
assert_eq!(worktrees.len(), 1,);
|
||||||
|
@ -1652,8 +1641,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let cx = &mut cx;
|
|
||||||
// generate some history to select from
|
// generate some history to select from
|
||||||
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
||||||
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
|
||||||
|
@ -1709,9 +1697,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from
|
||||||
let cx = &mut cx;
|
|
||||||
// generate some history to select from
|
|
||||||
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
|
||||||
open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
|
open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
|
||||||
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
|
open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
|
||||||
|
@ -1807,10 +1793,10 @@ mod tests {
|
||||||
) -> (
|
) -> (
|
||||||
View<Picker<FileFinderDelegate>>,
|
View<Picker<FileFinderDelegate>>,
|
||||||
View<Workspace>,
|
View<Workspace>,
|
||||||
VisualTestContext,
|
&mut VisualTestContext,
|
||||||
) {
|
) {
|
||||||
let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||||
let picker = open_file_picker(&workspace, &mut cx);
|
let picker = open_file_picker(&workspace, cx);
|
||||||
(picker, workspace, cx)
|
(picker, workspace, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ impl TestAppContext {
|
||||||
.any_handle
|
.any_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
|
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut ViewContext<V>) -> V,
|
F: FnOnce(&mut ViewContext<V>) -> V,
|
||||||
V: Render,
|
V: Render,
|
||||||
|
@ -149,7 +149,9 @@ impl TestAppContext {
|
||||||
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
|
||||||
drop(cx);
|
drop(cx);
|
||||||
let view = window.root_view(self).unwrap();
|
let view = window.root_view(self).unwrap();
|
||||||
(view, VisualTestContext::from_window(*window.deref(), self))
|
let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
|
||||||
|
// it might be nice to try and cleanup these at the end of each test.
|
||||||
|
(view, Box::leak(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn simulate_new_path_selection(
|
pub fn simulate_new_path_selection(
|
||||||
|
|
|
@ -10,21 +10,12 @@ pub trait Element<V: 'static> {
|
||||||
|
|
||||||
fn element_id(&self) -> Option<ElementId>;
|
fn element_id(&self) -> Option<ElementId>;
|
||||||
|
|
||||||
/// Called to initialize this element for the current frame. If this
|
|
||||||
/// element had state in a previous frame, it will be passed in for the 3rd argument.
|
|
||||||
fn initialize(
|
|
||||||
&mut self,
|
|
||||||
view_state: &mut V,
|
|
||||||
element_state: Option<Self::ElementState>,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::ElementState;
|
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: &mut Self::ElementState,
|
previous_element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> LayoutId;
|
) -> (LayoutId, Self::ElementState);
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -96,7 +87,6 @@ pub trait ParentComponent<V: 'static> {
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ElementObject<V> {
|
trait ElementObject<V> {
|
||||||
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
|
||||||
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
|
fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
|
||||||
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
|
||||||
fn measure(
|
fn measure(
|
||||||
|
@ -123,9 +113,6 @@ struct RenderedElement<V: 'static, E: Element<V>> {
|
||||||
enum ElementRenderPhase<V> {
|
enum ElementRenderPhase<V> {
|
||||||
#[default]
|
#[default]
|
||||||
Start,
|
Start,
|
||||||
Initialized {
|
|
||||||
frame_state: Option<V>,
|
|
||||||
},
|
|
||||||
LayoutRequested {
|
LayoutRequested {
|
||||||
layout_id: LayoutId,
|
layout_id: LayoutId,
|
||||||
frame_state: Option<V>,
|
frame_state: Option<V>,
|
||||||
|
@ -157,42 +144,19 @@ where
|
||||||
E: Element<V>,
|
E: Element<V>,
|
||||||
E::ElementState: 'static,
|
E::ElementState: 'static,
|
||||||
{
|
{
|
||||||
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
|
||||||
let frame_state = if let Some(id) = self.element.element_id() {
|
|
||||||
cx.with_element_state(id, |element_state, cx| {
|
|
||||||
let element_state = self.element.initialize(view_state, element_state, cx);
|
|
||||||
((), element_state)
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let frame_state = self.element.initialize(view_state, None, cx);
|
|
||||||
Some(frame_state)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.phase = ElementRenderPhase::Initialized { frame_state };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||||
let layout_id;
|
let (layout_id, frame_state) = match mem::take(&mut self.phase) {
|
||||||
let mut frame_state;
|
ElementRenderPhase::Start => {
|
||||||
match mem::take(&mut self.phase) {
|
|
||||||
ElementRenderPhase::Initialized {
|
|
||||||
frame_state: initial_frame_state,
|
|
||||||
} => {
|
|
||||||
frame_state = initial_frame_state;
|
|
||||||
if let Some(id) = self.element.element_id() {
|
if let Some(id) = self.element.element_id() {
|
||||||
layout_id = cx.with_element_state(id, |element_state, cx| {
|
let layout_id = cx.with_element_state(id, |element_state, cx| {
|
||||||
let mut element_state = element_state.unwrap();
|
self.element.layout(state, element_state, cx)
|
||||||
let layout_id = self.element.layout(state, &mut element_state, cx);
|
|
||||||
(layout_id, element_state)
|
|
||||||
});
|
});
|
||||||
|
(layout_id, None)
|
||||||
} else {
|
} else {
|
||||||
layout_id = self
|
let (layout_id, frame_state) = self.element.layout(state, None, cx);
|
||||||
.element
|
(layout_id, Some(frame_state))
|
||||||
.layout(state, frame_state.as_mut().unwrap(), cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ElementRenderPhase::Start => panic!("must call initialize before layout"),
|
|
||||||
ElementRenderPhase::LayoutRequested { .. }
|
ElementRenderPhase::LayoutRequested { .. }
|
||||||
| ElementRenderPhase::LayoutComputed { .. }
|
| ElementRenderPhase::LayoutComputed { .. }
|
||||||
| ElementRenderPhase::Painted { .. } => {
|
| ElementRenderPhase::Painted { .. } => {
|
||||||
|
@ -244,10 +208,6 @@ where
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Size<Pixels> {
|
) -> Size<Pixels> {
|
||||||
if matches!(&self.phase, ElementRenderPhase::Start) {
|
if matches!(&self.phase, ElementRenderPhase::Start) {
|
||||||
self.initialize(view_state, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) {
|
|
||||||
self.layout(view_state, cx);
|
self.layout(view_state, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,16 +244,13 @@ where
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
available_space: Size<AvailableSpace>,
|
available_space: Size<AvailableSpace>,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) {
|
) {
|
||||||
self.measure(available_space, view_state, cx);
|
self.measure(available_space, view_state, cx);
|
||||||
// Ignore the element offset when drawing this element, as the origin is already specified
|
cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
|
||||||
// in absolute terms.
|
|
||||||
origin -= cx.element_offset();
|
|
||||||
cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,10 +266,6 @@ impl<V> AnyElement<V> {
|
||||||
AnyElement(Box::new(RenderedElement::new(element)))
|
AnyElement(Box::new(RenderedElement::new(element)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
|
|
||||||
self.0.initialize(view_state, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
|
||||||
self.0.layout(view_state, cx)
|
self.0.layout(view_state, cx)
|
||||||
}
|
}
|
||||||
|
@ -393,25 +346,16 @@ where
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
|
||||||
&mut self,
|
|
||||||
view_state: &mut V,
|
|
||||||
_rendered_element: Option<Self::ElementState>,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::ElementState {
|
|
||||||
let render = self.take().unwrap();
|
|
||||||
let mut rendered_element = (render)(view_state, cx).render();
|
|
||||||
rendered_element.initialize(view_state, cx);
|
|
||||||
rendered_element
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
rendered_element: &mut Self::ElementState,
|
_: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> LayoutId {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
rendered_element.layout(view_state, cx)
|
let render = self.take().unwrap();
|
||||||
|
let mut rendered_element = (render)(view_state, cx).render();
|
||||||
|
let layout_id = rendered_element.layout(view_state, cx);
|
||||||
|
(layout_id, rendered_element)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
|
|
@ -22,7 +22,6 @@ use util::ResultExt;
|
||||||
|
|
||||||
const DRAG_THRESHOLD: f64 = 2.;
|
const DRAG_THRESHOLD: f64 = 2.;
|
||||||
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
|
const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
|
||||||
const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
|
|
||||||
|
|
||||||
pub struct GroupStyle {
|
pub struct GroupStyle {
|
||||||
pub group: SharedString,
|
pub group: SharedString,
|
||||||
|
@ -408,21 +407,19 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tooltip<W>(
|
fn tooltip(
|
||||||
mut self,
|
mut self,
|
||||||
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
|
build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
W: 'static + Render,
|
|
||||||
{
|
{
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
self.interactivity().tooltip_builder.is_none(),
|
self.interactivity().tooltip_builder.is_none(),
|
||||||
"calling tooltip more than once on the same element is not supported"
|
"calling tooltip more than once on the same element is not supported"
|
||||||
);
|
);
|
||||||
self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| {
|
self.interactivity().tooltip_builder =
|
||||||
build_tooltip(view_state, cx).into()
|
Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
|
||||||
}));
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -437,14 +434,6 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.interactivity().focus_in_style = f(StyleRefinement::default());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
|
@ -617,46 +606,36 @@ impl<V: 'static> Element<V> for Div<V> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
let interactive_state = self
|
let mut child_layout_ids = SmallVec::new();
|
||||||
.interactivity
|
|
||||||
.initialize(element_state.map(|s| s.interactive_state), cx);
|
|
||||||
|
|
||||||
for child in &mut self.children {
|
|
||||||
child.initialize(view_state, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
DivState {
|
|
||||||
interactive_state,
|
|
||||||
child_layout_ids: SmallVec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> crate::LayoutId {
|
|
||||||
let mut interactivity = mem::take(&mut self.interactivity);
|
let mut interactivity = mem::take(&mut self.interactivity);
|
||||||
let layout_id =
|
let (layout_id, interactive_state) = interactivity.layout(
|
||||||
interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
|
element_state.map(|s| s.interactive_state),
|
||||||
|
cx,
|
||||||
|
|style, cx| {
|
||||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||||
element_state.child_layout_ids = self
|
child_layout_ids = self
|
||||||
.children
|
.children
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.map(|child| child.layout(view_state, cx))
|
.map(|child| child.layout(view_state, cx))
|
||||||
.collect::<SmallVec<_>>();
|
.collect::<SmallVec<_>>();
|
||||||
cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
|
cx.request_layout(&style, child_layout_ids.iter().copied())
|
||||||
})
|
})
|
||||||
});
|
},
|
||||||
|
);
|
||||||
self.interactivity = interactivity;
|
self.interactivity = interactivity;
|
||||||
layout_id
|
(
|
||||||
|
layout_id,
|
||||||
|
DivState {
|
||||||
|
interactive_state,
|
||||||
|
child_layout_ids,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
@ -740,7 +719,6 @@ pub struct Interactivity<V> {
|
||||||
pub group: Option<SharedString>,
|
pub group: Option<SharedString>,
|
||||||
pub base_style: StyleRefinement,
|
pub base_style: StyleRefinement,
|
||||||
pub focus_style: StyleRefinement,
|
pub focus_style: StyleRefinement,
|
||||||
pub focus_in_style: StyleRefinement,
|
|
||||||
pub in_focus_style: StyleRefinement,
|
pub in_focus_style: StyleRefinement,
|
||||||
pub hover_style: StyleRefinement,
|
pub hover_style: StyleRefinement,
|
||||||
pub group_hover_style: Option<GroupStyle>,
|
pub group_hover_style: Option<GroupStyle>,
|
||||||
|
@ -766,11 +744,12 @@ impl<V> Interactivity<V>
|
||||||
where
|
where
|
||||||
V: 'static,
|
V: 'static,
|
||||||
{
|
{
|
||||||
pub fn initialize(
|
pub fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
element_state: Option<InteractiveElementState>,
|
element_state: Option<InteractiveElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> InteractiveElementState {
|
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
|
||||||
|
) -> (LayoutId, InteractiveElementState) {
|
||||||
let mut element_state = element_state.unwrap_or_default();
|
let mut element_state = element_state.unwrap_or_default();
|
||||||
|
|
||||||
// Ensure we store a focus handle in our element state if we're focusable.
|
// Ensure we store a focus handle in our element state if we're focusable.
|
||||||
|
@ -785,17 +764,9 @@ where
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
element_state
|
let style = self.compute_style(None, &mut element_state, cx);
|
||||||
}
|
let layout_id = f(style, cx);
|
||||||
|
(layout_id, element_state)
|
||||||
pub fn layout(
|
|
||||||
&mut self,
|
|
||||||
element_state: &mut InteractiveElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
|
|
||||||
) -> LayoutId {
|
|
||||||
let style = self.compute_style(None, element_state, cx);
|
|
||||||
f(style, cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paint(
|
pub fn paint(
|
||||||
|
@ -992,7 +963,7 @@ where
|
||||||
waiting: None,
|
waiting: None,
|
||||||
tooltip: Some(AnyTooltip {
|
tooltip: Some(AnyTooltip {
|
||||||
view: tooltip_builder(view_state, cx),
|
view: tooltip_builder(view_state, cx),
|
||||||
cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
|
cursor_offset: cx.mouse_position(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1130,10 +1101,6 @@ where
|
||||||
style.refine(&self.base_style);
|
style.refine(&self.base_style);
|
||||||
|
|
||||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||||
if focus_handle.contains_focused(cx) {
|
|
||||||
style.refine(&self.focus_in_style);
|
|
||||||
}
|
|
||||||
|
|
||||||
if focus_handle.within_focused(cx) {
|
if focus_handle.within_focused(cx) {
|
||||||
style.refine(&self.in_focus_style);
|
style.refine(&self.in_focus_style);
|
||||||
}
|
}
|
||||||
|
@ -1206,7 +1173,6 @@ impl<V: 'static> Default for Interactivity<V> {
|
||||||
group: None,
|
group: None,
|
||||||
base_style: StyleRefinement::default(),
|
base_style: StyleRefinement::default(),
|
||||||
focus_style: StyleRefinement::default(),
|
focus_style: StyleRefinement::default(),
|
||||||
focus_in_style: StyleRefinement::default(),
|
|
||||||
in_focus_style: StyleRefinement::default(),
|
in_focus_style: StyleRefinement::default(),
|
||||||
hover_style: StyleRefinement::default(),
|
hover_style: StyleRefinement::default(),
|
||||||
group_hover_style: None,
|
group_hover_style: None,
|
||||||
|
@ -1327,21 +1293,12 @@ where
|
||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
self.element.initialize(view_state, element_state, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> LayoutId {
|
|
||||||
self.element.layout(view_state, element_state, cx)
|
self.element.layout(view_state, element_state, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1422,21 +1379,12 @@ where
|
||||||
self.element.element_id()
|
self.element.element_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
self.element.initialize(view_state, element_state, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> LayoutId {
|
|
||||||
self.element.layout(view_state, element_state, cx)
|
self.element.layout(view_state, element_state, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,21 +48,12 @@ impl<V> Element<V> for Img<V> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut V,
|
_view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
self.interactivity.initialize(element_state, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
_view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> LayoutId {
|
|
||||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||||
cx.request_layout(&style, None)
|
cx.request_layout(&style, None)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
mod div;
|
mod div;
|
||||||
mod img;
|
mod img;
|
||||||
|
mod overlay;
|
||||||
mod svg;
|
mod svg;
|
||||||
mod text;
|
mod text;
|
||||||
mod uniform_list;
|
mod uniform_list;
|
||||||
|
|
||||||
pub use div::*;
|
pub use div::*;
|
||||||
pub use img::*;
|
pub use img::*;
|
||||||
|
pub use overlay::*;
|
||||||
pub use svg::*;
|
pub use svg::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use uniform_list::*;
|
pub use uniform_list::*;
|
||||||
|
|
203
crates/gpui2/src/elements/overlay.rs
Normal file
203
crates/gpui2/src/elements/overlay.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
|
||||||
|
Size, Style,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct OverlayState {
|
||||||
|
child_layout_ids: SmallVec<[LayoutId; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Overlay<V> {
|
||||||
|
children: SmallVec<[AnyElement<V>; 2]>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
|
fit_mode: OverlayFitMode,
|
||||||
|
// todo!();
|
||||||
|
// anchor_position: Option<Vector2F>,
|
||||||
|
// position_mode: OverlayPositionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// overlay gives you a floating element that will avoid overflowing the window bounds.
|
||||||
|
/// Its children should have no margin to avoid measurement issues.
|
||||||
|
pub fn overlay<V: 'static>() -> Overlay<V> {
|
||||||
|
Overlay {
|
||||||
|
children: SmallVec::new(),
|
||||||
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
|
fit_mode: OverlayFitMode::SwitchAnchor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Overlay<V> {
|
||||||
|
/// Sets which corner of the overlay should be anchored to the current position.
|
||||||
|
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
|
||||||
|
self.anchor_corner = anchor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snap to window edge instead of switching anchor corner when an overflow would occur.
|
||||||
|
pub fn snap_to_window(mut self) -> Self {
|
||||||
|
self.fit_mode = OverlayFitMode::SnapToWindow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> ParentComponent<V> for Overlay<V> {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static> Element<V> for Overlay<V> {
|
||||||
|
type ElementState = OverlayState;
|
||||||
|
|
||||||
|
fn element_id(&self) -> Option<crate::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
view_state: &mut V,
|
||||||
|
_: Option<Self::ElementState>,
|
||||||
|
cx: &mut crate::ViewContext<V>,
|
||||||
|
) -> (crate::LayoutId, Self::ElementState) {
|
||||||
|
let child_layout_ids = self
|
||||||
|
.children
|
||||||
|
.iter_mut()
|
||||||
|
.map(|child| child.layout(view_state, cx))
|
||||||
|
.collect::<SmallVec<_>>();
|
||||||
|
let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
|
||||||
|
|
||||||
|
(layout_id, OverlayState { child_layout_ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: crate::Bounds<crate::Pixels>,
|
||||||
|
view_state: &mut V,
|
||||||
|
element_state: &mut Self::ElementState,
|
||||||
|
cx: &mut crate::ViewContext<V>,
|
||||||
|
) {
|
||||||
|
if element_state.child_layout_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child_min = point(Pixels::MAX, Pixels::MAX);
|
||||||
|
let mut child_max = Point::default();
|
||||||
|
for child_layout_id in &element_state.child_layout_ids {
|
||||||
|
let child_bounds = cx.layout_bounds(*child_layout_id);
|
||||||
|
child_min = child_min.min(&child_bounds.origin);
|
||||||
|
child_max = child_max.max(&child_bounds.lower_right());
|
||||||
|
}
|
||||||
|
let size: Size<Pixels> = (child_max - child_min).into();
|
||||||
|
let origin = bounds.origin;
|
||||||
|
|
||||||
|
let mut desired = self.anchor_corner.get_bounds(origin, size);
|
||||||
|
let limits = Bounds {
|
||||||
|
origin: Point::zero(),
|
||||||
|
size: cx.viewport_size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.fit_mode {
|
||||||
|
OverlayFitMode::SnapToWindow => {
|
||||||
|
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||||
|
// its horizontal bounds overflow
|
||||||
|
if desired.right() > limits.right() {
|
||||||
|
desired.origin.x -= desired.right() - limits.right();
|
||||||
|
} else if desired.left() < limits.left() {
|
||||||
|
desired.origin.x = limits.origin.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap the vertical edges of the overlay to the vertical edges of the window if
|
||||||
|
// its vertical bounds overflow.
|
||||||
|
if desired.bottom() > limits.bottom() {
|
||||||
|
desired.origin.y -= desired.bottom() - limits.bottom();
|
||||||
|
} else if desired.top() < limits.top() {
|
||||||
|
desired.origin.y = limits.origin.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OverlayFitMode::SwitchAnchor => {
|
||||||
|
let mut anchor_corner = self.anchor_corner;
|
||||||
|
|
||||||
|
if desired.left() < limits.left() || desired.right() > limits.right() {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bounds if needed
|
||||||
|
if anchor_corner != self.anchor_corner {
|
||||||
|
desired = anchor_corner.get_bounds(origin, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OverlayFitMode::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
|
||||||
|
for child in &mut self.children {
|
||||||
|
child.paint(view_state, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Axis {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum OverlayFitMode {
|
||||||
|
SnapToWindow,
|
||||||
|
SwitchAnchor,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AnchorCorner {
|
||||||
|
TopLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnchorCorner {
|
||||||
|
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
|
||||||
|
let origin = match self {
|
||||||
|
Self::TopLeft => origin,
|
||||||
|
Self::TopRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y,
|
||||||
|
},
|
||||||
|
Self::BottomLeft => Point {
|
||||||
|
x: origin.x,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
Self::BottomRight => Point {
|
||||||
|
x: origin.x - size.width,
|
||||||
|
y: origin.y - size.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds { origin, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_axis(self, axis: Axis) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::Vertical => match self {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::TopRight,
|
||||||
|
},
|
||||||
|
Axis::Horizontal => match self {
|
||||||
|
AnchorCorner::TopLeft => AnchorCorner::TopRight,
|
||||||
|
AnchorCorner::TopRight => AnchorCorner::TopLeft,
|
||||||
|
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
|
||||||
|
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,21 +37,12 @@ impl<V> Element<V> for Svg<V> {
|
||||||
self.interactivity.element_id.clone()
|
self.interactivity.element_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut V,
|
_view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
self.interactivity.initialize(element_state, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
_view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> LayoutId {
|
|
||||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||||
cx.request_layout(&style, None)
|
cx.request_layout(&style, None)
|
||||||
})
|
})
|
||||||
|
|
|
@ -76,21 +76,13 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
|
||||||
&mut self,
|
|
||||||
_view_state: &mut V,
|
|
||||||
element_state: Option<Self::ElementState>,
|
|
||||||
_cx: &mut ViewContext<V>,
|
|
||||||
) -> Self::ElementState {
|
|
||||||
element_state.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view: &mut V,
|
_view: &mut V,
|
||||||
element_state: &mut Self::ElementState,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> LayoutId {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
|
let element_state = element_state.unwrap_or_default();
|
||||||
let text_system = cx.text_system().clone();
|
let text_system = cx.text_system().clone();
|
||||||
let text_style = cx.text_style();
|
let text_style = cx.text_style();
|
||||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||||
|
@ -148,7 +140,7 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
layout_id
|
(layout_id, element_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
|
|
@ -108,62 +108,54 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||||
Some(self.id.clone())
|
Some(self.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
view_state: &mut V,
|
view_state: &mut V,
|
||||||
element_state: Option<Self::ElementState>,
|
element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<V>,
|
cx: &mut ViewContext<V>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
if let Some(mut element_state) = element_state {
|
|
||||||
element_state.interactive = self
|
|
||||||
.interactivity
|
|
||||||
.initialize(Some(element_state.interactive), cx);
|
|
||||||
element_state
|
|
||||||
} else {
|
|
||||||
let item_size = self.measure_item(view_state, None, cx);
|
|
||||||
UniformListState {
|
|
||||||
interactive: self.interactivity.initialize(None, cx),
|
|
||||||
item_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
_view_state: &mut V,
|
|
||||||
element_state: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<V>,
|
|
||||||
) -> LayoutId {
|
|
||||||
let max_items = self.item_count;
|
let max_items = self.item_count;
|
||||||
let item_size = element_state.item_size;
|
|
||||||
let rem_size = cx.rem_size();
|
let rem_size = cx.rem_size();
|
||||||
|
let item_size = element_state
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.item_size)
|
||||||
|
.unwrap_or_else(|| self.measure_item(view_state, None, cx));
|
||||||
|
|
||||||
self.interactivity
|
let (layout_id, interactive) =
|
||||||
.layout(&mut element_state.interactive, cx, |style, cx| {
|
self.interactivity
|
||||||
cx.request_measured_layout(
|
.layout(element_state.map(|s| s.interactive), cx, |style, cx| {
|
||||||
style,
|
cx.request_measured_layout(
|
||||||
rem_size,
|
style,
|
||||||
move |known_dimensions: Size<Option<Pixels>>,
|
rem_size,
|
||||||
available_space: Size<AvailableSpace>| {
|
move |known_dimensions: Size<Option<Pixels>>,
|
||||||
let desired_height = item_size.height * max_items;
|
available_space: Size<AvailableSpace>| {
|
||||||
let width = known_dimensions
|
let desired_height = item_size.height * max_items;
|
||||||
.width
|
let width =
|
||||||
.unwrap_or(match available_space.width {
|
known_dimensions
|
||||||
AvailableSpace::Definite(x) => x,
|
.width
|
||||||
|
.unwrap_or(match available_space.width {
|
||||||
|
AvailableSpace::Definite(x) => x,
|
||||||
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
|
item_size.width
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let height = match available_space.height {
|
||||||
|
AvailableSpace::Definite(x) => desired_height.min(x),
|
||||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||||
item_size.width
|
desired_height
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
let height = match available_space.height {
|
size(width, height)
|
||||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
},
|
||||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
)
|
||||||
desired_height
|
});
|
||||||
}
|
|
||||||
};
|
let element_state = UniformListState {
|
||||||
size(width, height)
|
interactive,
|
||||||
},
|
item_size,
|
||||||
)
|
};
|
||||||
})
|
|
||||||
|
(layout_id, element_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
|
|
@ -421,6 +421,22 @@ impl<T> Bounds<T>
|
||||||
where
|
where
|
||||||
T: Add<T, Output = T> + Clone + Default + Debug,
|
T: Add<T, Output = T> + Clone + Default + Debug,
|
||||||
{
|
{
|
||||||
|
pub fn top(&self) -> T {
|
||||||
|
self.origin.y.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bottom(&self) -> T {
|
||||||
|
self.origin.y.clone() + self.size.height.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left(&self) -> T {
|
||||||
|
self.origin.x.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(&self) -> T {
|
||||||
|
self.origin.x.clone() + self.size.width.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn upper_right(&self) -> Point<T> {
|
pub fn upper_right(&self) -> Point<T> {
|
||||||
Point {
|
Point {
|
||||||
x: self.origin.x.clone() + self.size.width.clone(),
|
x: self.origin.x.clone() + self.size.width.clone(),
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||||
Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView,
|
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||||
LayoutId, Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
|
FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -162,8 +163,7 @@ impl<V> Eq for WeakView<V> {}
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AnyView {
|
pub struct AnyView {
|
||||||
model: AnyModel,
|
model: AnyModel,
|
||||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
|
||||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,6 @@ impl AnyView {
|
||||||
pub fn downgrade(&self) -> AnyWeakView {
|
pub fn downgrade(&self) -> AnyWeakView {
|
||||||
AnyWeakView {
|
AnyWeakView {
|
||||||
model: self.model.downgrade(),
|
model: self.model.downgrade(),
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
}
|
}
|
||||||
|
@ -182,7 +181,6 @@ impl AnyView {
|
||||||
Ok(model) => Ok(View { model }),
|
Ok(model) => Ok(View { model }),
|
||||||
Err(model) => Err(Self {
|
Err(model) => Err(Self {
|
||||||
model,
|
model,
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
}),
|
}),
|
||||||
|
@ -193,13 +191,19 @@ impl AnyView {
|
||||||
self.model.entity_type
|
self.model.entity_type
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn draw(&self, available_space: Size<AvailableSpace>, cx: &mut WindowContext) {
|
pub(crate) fn draw(
|
||||||
let mut rendered_element = (self.initialize)(self, cx);
|
&self,
|
||||||
let layout_id = (self.layout)(self, &mut rendered_element, cx);
|
origin: Point<Pixels>,
|
||||||
cx.window
|
available_space: Size<AvailableSpace>,
|
||||||
.layout_engine
|
cx: &mut WindowContext,
|
||||||
.compute_layout(layout_id, available_space);
|
) {
|
||||||
(self.paint)(self, &mut rendered_element, cx);
|
cx.with_absolute_element_offset(origin, |cx| {
|
||||||
|
let (layout_id, mut rendered_element) = (self.layout)(self, cx);
|
||||||
|
cx.window
|
||||||
|
.layout_engine
|
||||||
|
.compute_layout(layout_id, available_space);
|
||||||
|
(self.paint)(self, &mut rendered_element, cx);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +217,6 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||||
fn from(value: View<V>) -> Self {
|
fn from(value: View<V>) -> Self {
|
||||||
AnyView {
|
AnyView {
|
||||||
model: value.model.into_any(),
|
model: value.model.into_any(),
|
||||||
initialize: any_view::initialize::<V>,
|
|
||||||
layout: any_view::layout::<V>,
|
layout: any_view::layout::<V>,
|
||||||
paint: any_view::paint::<V>,
|
paint: any_view::paint::<V>,
|
||||||
}
|
}
|
||||||
|
@ -227,22 +230,13 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||||
Some(self.model.entity_id.into())
|
Some(self.model.entity_id.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_view_state: &mut ParentViewState,
|
_view_state: &mut ParentViewState,
|
||||||
_element_state: Option<Self::ElementState>,
|
_element_state: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
cx: &mut ViewContext<ParentViewState>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
(self.initialize)(self, cx)
|
(self.layout)(self, cx)
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
_view_state: &mut ParentViewState,
|
|
||||||
rendered_element: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
|
||||||
) -> LayoutId {
|
|
||||||
(self.layout)(self, rendered_element, cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
|
@ -258,8 +252,7 @@ impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||||
|
|
||||||
pub struct AnyWeakView {
|
pub struct AnyWeakView {
|
||||||
model: AnyWeakModel,
|
model: AnyWeakModel,
|
||||||
initialize: fn(&AnyView, &mut WindowContext) -> AnyBox,
|
layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
|
||||||
layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId,
|
|
||||||
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +261,6 @@ impl AnyWeakView {
|
||||||
let model = self.model.upgrade()?;
|
let model = self.model.upgrade()?;
|
||||||
Some(AnyView {
|
Some(AnyView {
|
||||||
model,
|
model,
|
||||||
initialize: self.initialize,
|
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
paint: self.paint,
|
paint: self.paint,
|
||||||
})
|
})
|
||||||
|
@ -279,7 +271,6 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
|
||||||
fn from(view: WeakView<V>) -> Self {
|
fn from(view: WeakView<V>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
model: view.model.into(),
|
model: view.model.into(),
|
||||||
initialize: any_view::initialize::<V>,
|
|
||||||
layout: any_view::layout::<V>,
|
layout: any_view::layout::<V>,
|
||||||
paint: any_view::paint::<V>,
|
paint: any_view::paint::<V>,
|
||||||
}
|
}
|
||||||
|
@ -326,28 +317,19 @@ where
|
||||||
Some(self.view.entity_id().into())
|
Some(self.view.entity_id().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &mut ParentViewState,
|
_: &mut ParentViewState,
|
||||||
_: Option<Self::ElementState>,
|
_: Option<Self::ElementState>,
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
cx: &mut ViewContext<ParentViewState>,
|
||||||
) -> Self::ElementState {
|
) -> (LayoutId, Self::ElementState) {
|
||||||
self.view.update(cx, |view, cx| {
|
self.view.update(cx, |view, cx| {
|
||||||
let mut element = self.component.take().unwrap().render();
|
let mut element = self.component.take().unwrap().render();
|
||||||
element.initialize(view, cx);
|
let layout_id = element.layout(view, cx);
|
||||||
element
|
(layout_id, element)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&mut self,
|
|
||||||
_: &mut ParentViewState,
|
|
||||||
element: &mut Self::ElementState,
|
|
||||||
cx: &mut ViewContext<ParentViewState>,
|
|
||||||
) -> LayoutId {
|
|
||||||
self.view.update(cx, |view, cx| element.layout(view, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Bounds<Pixels>,
|
_: Bounds<Pixels>,
|
||||||
|
@ -363,27 +345,17 @@ mod any_view {
|
||||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
|
|
||||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
|
||||||
let view = view.clone().downcast::<V>().unwrap();
|
|
||||||
let element = view.update(cx, |view, cx| {
|
|
||||||
let mut element = AnyElement::new(view.render(cx));
|
|
||||||
element.initialize(view, cx);
|
|
||||||
element
|
|
||||||
});
|
|
||||||
Box::new(element)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn layout<V: Render>(
|
pub(crate) fn layout<V: Render>(
|
||||||
view: &AnyView,
|
view: &AnyView,
|
||||||
element: &mut Box<dyn Any>,
|
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> LayoutId {
|
) -> (LayoutId, Box<dyn Any>) {
|
||||||
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
cx.with_element_id(Some(view.model.entity_id), |cx| {
|
||||||
let view = view.clone().downcast::<V>().unwrap();
|
let view = view.clone().downcast::<V>().unwrap();
|
||||||
let element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
view.update(cx, |view, cx| {
|
||||||
view.update(cx, |view, cx| element.layout(view, cx))
|
let mut element = AnyElement::new(view.render(cx));
|
||||||
|
let layout_id = element.layout(view, cx);
|
||||||
|
(layout_id, Box::new(element) as Box<dyn Any>)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1080,26 +1080,22 @@ impl<'a> WindowContext<'a> {
|
||||||
|
|
||||||
self.with_z_index(0, |cx| {
|
self.with_z_index(0, |cx| {
|
||||||
let available_space = cx.window.viewport_size.map(Into::into);
|
let available_space = cx.window.viewport_size.map(Into::into);
|
||||||
root_view.draw(available_space, cx);
|
root_view.draw(Point::zero(), available_space, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(active_drag) = self.app.active_drag.take() {
|
if let Some(active_drag) = self.app.active_drag.take() {
|
||||||
self.with_z_index(1, |cx| {
|
self.with_z_index(1, |cx| {
|
||||||
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
let offset = cx.mouse_position() - active_drag.cursor_offset;
|
||||||
cx.with_element_offset(offset, |cx| {
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
let available_space =
|
active_drag.view.draw(offset, available_space, cx);
|
||||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
cx.active_drag = Some(active_drag);
|
||||||
active_drag.view.draw(available_space, cx);
|
|
||||||
cx.active_drag = Some(active_drag);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
} else if let Some(active_tooltip) = self.app.active_tooltip.take() {
|
||||||
self.with_z_index(1, |cx| {
|
self.with_z_index(1, |cx| {
|
||||||
cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
|
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||||
let available_space =
|
active_tooltip
|
||||||
size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
.view
|
||||||
active_tooltip.view.draw(available_space, cx);
|
.draw(active_tooltip.cursor_offset, available_space, cx);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1643,8 +1639,8 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the global element offset based on the given offset. This is used to implement
|
/// Update the global element offset relative to the current offset. This is used to implement
|
||||||
/// scrolling and position drag handles.
|
/// scrolling.
|
||||||
fn with_element_offset<R>(
|
fn with_element_offset<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
offset: Point<Pixels>,
|
offset: Point<Pixels>,
|
||||||
|
@ -1654,7 +1650,17 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||||
return f(self);
|
return f(self);
|
||||||
};
|
};
|
||||||
|
|
||||||
let offset = self.element_offset() + offset;
|
let abs_offset = self.element_offset() + offset;
|
||||||
|
self.with_absolute_element_offset(abs_offset, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the global element offset based on the given offset. This is used to implement
|
||||||
|
/// drag handles and other manual painting of elements.
|
||||||
|
fn with_absolute_element_offset<R>(
|
||||||
|
&mut self,
|
||||||
|
offset: Point<Pixels>,
|
||||||
|
f: impl FnOnce(&mut Self) -> R,
|
||||||
|
) -> R {
|
||||||
self.window_mut()
|
self.window_mut()
|
||||||
.current_frame
|
.current_frame
|
||||||
.element_offset_stack
|
.element_offset_stack
|
||||||
|
|
|
@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) {
|
||||||
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
|
||||||
init_settings(cx);
|
init_settings(cx);
|
||||||
file_associations::init(assets, cx);
|
file_associations::init(assets, cx);
|
||||||
|
|
||||||
|
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||||
|
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||||
|
workspace.toggle_panel_focus::<ProjectPanel>(cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -1515,12 +1522,12 @@ impl workspace::dock::Panel for ProjectPanel {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
|
||||||
Some("icons/project.svg")
|
Some(ui::Icon::FileTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self) -> Box<dyn Action> {
|
||||||
("Project Panel".into(), Some(Box::new(ToggleFocus)))
|
Box::new(ToggleFocus)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_focus(&self, _: &WindowContext) -> bool {
|
fn has_focus(&self, _: &WindowContext) -> bool {
|
||||||
|
|
|
@ -57,7 +57,6 @@ impl Render for FocusStory {
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(color_1)
|
.bg(color_1)
|
||||||
.focus(|style| style.bg(color_2))
|
.focus(|style| style.bg(color_2))
|
||||||
.focus_in(|style| style.bg(color_3))
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.child_1_focus)
|
.track_focus(&self.child_1_focus)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
|
use ui::Tooltip;
|
||||||
|
|
||||||
pub struct ScrollStory;
|
pub struct ScrollStory;
|
||||||
|
|
||||||
|
@ -35,16 +36,18 @@ impl Render for ScrollStory {
|
||||||
} else {
|
} else {
|
||||||
color_2
|
color_2
|
||||||
};
|
};
|
||||||
div().id(id).bg(bg).size(px(100. as f32)).when(
|
div()
|
||||||
row >= 5 && column >= 5,
|
.id(id)
|
||||||
|d| {
|
.tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx))
|
||||||
|
.bg(bg)
|
||||||
|
.size(px(100. as f32))
|
||||||
|
.when(row >= 5 && column >= 5, |d| {
|
||||||
d.overflow_scroll()
|
d.overflow_scroll()
|
||||||
.child(div().size(px(50.)).bg(color_1))
|
.child(div().size(px(50.)).bg(color_1))
|
||||||
.child(div().size(px(50.)).bg(color_2))
|
.child(div().size(px(50.)).bg(color_2))
|
||||||
.child(div().size(px(50.)).bg(color_1))
|
.child(div().size(px(50.)).bg(color_1))
|
||||||
.child(div().size(px(50.)).bg(color_2))
|
.child(div().size(px(50.)).bg(color_2))
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ impl ButtonVariant {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) + Send + Sync>;
|
pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
|
||||||
|
|
||||||
struct ButtonHandlers<V: 'static> {
|
struct ButtonHandlers<V: 'static> {
|
||||||
click: Option<ClickHandler<V>>,
|
click: Option<ClickHandler<V>>,
|
||||||
|
|
|
@ -25,6 +25,7 @@ pub enum Icon {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Close,
|
Close,
|
||||||
|
Collab,
|
||||||
Dash,
|
Dash,
|
||||||
Exit,
|
Exit,
|
||||||
ExclamationTriangle,
|
ExclamationTriangle,
|
||||||
|
@ -83,6 +84,7 @@ impl Icon {
|
||||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||||
Icon::Close => "icons/x.svg",
|
Icon::Close => "icons/x.svg",
|
||||||
|
Icon::Collab => "icons/user_group_16.svg",
|
||||||
Icon::Dash => "icons/dash.svg",
|
Icon::Dash => "icons/dash.svg",
|
||||||
Icon::Exit => "icons/exit.svg",
|
Icon::Exit => "icons/exit.svg",
|
||||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
|
use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
|
||||||
use gpui::{prelude::*, MouseButton, VisualContext};
|
use gpui::{prelude::*, AnyView, MouseButton};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
struct IconButtonHandlers<V: 'static> {
|
struct IconButtonHandlers<V: 'static> {
|
||||||
|
@ -19,7 +19,7 @@ pub struct IconButton<V: 'static> {
|
||||||
color: TextColor,
|
color: TextColor,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
state: InteractionState,
|
state: InteractionState,
|
||||||
tooltip: Option<SharedString>,
|
tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
|
||||||
handlers: IconButtonHandlers<V>,
|
handlers: IconButtonHandlers<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,22 +56,23 @@ impl<V: 'static> IconButton<V> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
pub fn tooltip(
|
||||||
self.tooltip = Some(tooltip.into());
|
mut self,
|
||||||
|
tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.tooltip = Some(Box::new(tooltip));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
|
||||||
mut self,
|
|
||||||
handler: impl 'static + Fn(&mut V, &mut ViewContext<V>) + Send + Sync,
|
|
||||||
) -> Self {
|
|
||||||
self.handlers.click = Some(Arc::new(handler));
|
self.handlers.click = Some(Arc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
let icon_color = match (self.state, self.color) {
|
let icon_color = match (self.state, self.color) {
|
||||||
(InteractionState::Disabled, _) => TextColor::Disabled,
|
(InteractionState::Disabled, _) => TextColor::Disabled,
|
||||||
|
(InteractionState::Active, _) => TextColor::Error,
|
||||||
_ => self.color,
|
_ => self.color,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,15 +100,16 @@ impl<V: 'static> IconButton<V> {
|
||||||
.child(IconElement::new(self.icon).color(icon_color));
|
.child(IconElement::new(self.icon).color(icon_color));
|
||||||
|
|
||||||
if let Some(click_handler) = self.handlers.click.clone() {
|
if let Some(click_handler) = self.handlers.click.clone() {
|
||||||
button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
button = button
|
||||||
cx.stop_propagation();
|
.on_mouse_down(MouseButton::Left, move |state, event, cx| {
|
||||||
click_handler(state, cx);
|
cx.stop_propagation();
|
||||||
});
|
click_handler(state, cx);
|
||||||
|
})
|
||||||
|
.cursor_pointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tooltip) = self.tooltip.clone() {
|
if let Some(tooltip) = self.tooltip.take() {
|
||||||
button =
|
button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
|
||||||
button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button
|
button
|
||||||
|
|
|
@ -1,17 +1,53 @@
|
||||||
use gpui::{Div, Render};
|
use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext};
|
||||||
use settings2::Settings;
|
use settings2::Settings;
|
||||||
use theme2::{ActiveTheme, ThemeSettings};
|
use theme2::{ActiveTheme, ThemeSettings};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
|
use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
|
||||||
|
|
||||||
pub struct TextTooltip {
|
pub struct Tooltip {
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
meta: Option<SharedString>,
|
meta: Option<SharedString>,
|
||||||
key_binding: Option<KeyBinding>,
|
key_binding: Option<KeyBinding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextTooltip {
|
impl Tooltip {
|
||||||
|
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
|
||||||
|
cx.build_view(|cx| Self {
|
||||||
|
title: title.into(),
|
||||||
|
meta: None,
|
||||||
|
key_binding: None,
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_action(
|
||||||
|
title: impl Into<SharedString>,
|
||||||
|
action: &dyn Action,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> AnyView {
|
||||||
|
cx.build_view(|cx| Self {
|
||||||
|
title: title.into(),
|
||||||
|
meta: None,
|
||||||
|
key_binding: KeyBinding::for_action(action, cx),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_meta(
|
||||||
|
title: impl Into<SharedString>,
|
||||||
|
action: Option<&dyn Action>,
|
||||||
|
meta: impl Into<SharedString>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> AnyView {
|
||||||
|
cx.build_view(|cx| Self {
|
||||||
|
title: title.into(),
|
||||||
|
meta: Some(meta.into()),
|
||||||
|
key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(title: impl Into<SharedString>) -> Self {
|
pub fn new(title: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
|
@ -31,31 +67,36 @@ impl TextTooltip {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TextTooltip {
|
impl Render for Tooltip {
|
||||||
type Element = Div<Self>;
|
type Element = Overlay<Self>;
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||||
v_stack()
|
overlay().child(
|
||||||
.elevation_2(cx)
|
// padding to avoid mouse cursor
|
||||||
.font(ui_font)
|
div().pl_2().pt_2p5().child(
|
||||||
.text_ui_sm()
|
v_stack()
|
||||||
.text_color(cx.theme().colors().text)
|
.elevation_2(cx)
|
||||||
.py_1()
|
.font(ui_font)
|
||||||
.px_2()
|
.text_ui_sm()
|
||||||
.child(
|
.text_color(cx.theme().colors().text)
|
||||||
h_stack()
|
.py_1()
|
||||||
.child(self.title.clone())
|
.px_2()
|
||||||
.when_some(self.key_binding.clone(), |this, key_binding| {
|
.child(
|
||||||
this.justify_between().child(key_binding)
|
h_stack()
|
||||||
|
.child(self.title.clone())
|
||||||
|
.when_some(self.key_binding.clone(), |this, key_binding| {
|
||||||
|
this.justify_between().child(key_binding)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(self.meta.clone(), |this, meta| {
|
||||||
|
this.child(
|
||||||
|
Label::new(meta)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(TextColor::Muted),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
),
|
||||||
.when_some(self.meta.clone(), |this, meta| {
|
)
|
||||||
this.child(
|
|
||||||
Label::new(meta)
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(TextColor::Muted),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use gpui::{
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use ui::{h_stack, IconButton, InteractionState, Tooltip};
|
||||||
|
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
ChangePosition,
|
ChangePosition,
|
||||||
|
@ -24,8 +25,8 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
||||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
|
||||||
fn size(&self, cx: &WindowContext) -> f32;
|
fn size(&self, cx: &WindowContext) -> f32;
|
||||||
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
|
fn toggle_action(&self) -> Box<dyn Action>;
|
||||||
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
fn icon_label(&self, _: &WindowContext) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -48,8 +49,8 @@ pub trait PanelHandle: Send + Sync {
|
||||||
fn set_active(&self, active: bool, cx: &mut WindowContext);
|
fn set_active(&self, active: bool, cx: &mut WindowContext);
|
||||||
fn size(&self, cx: &WindowContext) -> f32;
|
fn size(&self, cx: &WindowContext) -> f32;
|
||||||
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
|
fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon>;
|
||||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
|
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action>;
|
||||||
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
fn icon_label(&self, cx: &WindowContext) -> Option<String>;
|
||||||
fn has_focus(&self, cx: &WindowContext) -> bool;
|
fn has_focus(&self, cx: &WindowContext) -> bool;
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||||
|
@ -100,12 +101,12 @@ where
|
||||||
self.update(cx, |this, cx| this.set_size(size, cx))
|
self.update(cx, |this, cx| this.set_size(size, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> {
|
||||||
self.read(cx).icon_path(cx)
|
self.read(cx).icon(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self, cx: &WindowContext) -> Box<dyn Action> {
|
||||||
self.read(cx).icon_tooltip()
|
self.read(cx).toggle_action()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
|
||||||
|
@ -221,11 +222,11 @@ impl Dock {
|
||||||
// .find_map(|entry| entry.panel.as_any().clone().downcast())
|
// .find_map(|entry| entry.panel.as_any().clone().downcast())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
|
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
|
||||||
// self.panel_entries
|
self.panel_entries
|
||||||
// .iter()
|
.iter()
|
||||||
// .position(|entry| entry.panel.as_any().is::<T>())
|
.position(|entry| entry.panel.to_any().downcast::<T>().is_ok())
|
||||||
// }
|
}
|
||||||
|
|
||||||
pub fn panel_index_for_persistent_name(
|
pub fn panel_index_for_persistent_name(
|
||||||
&self,
|
&self,
|
||||||
|
@ -653,11 +654,28 @@ impl Render for PanelButtons {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
// todo!()
|
// todo!()
|
||||||
let dock = self.dock.read(cx);
|
let dock = self.dock.read(cx);
|
||||||
div().children(
|
let active_index = dock.active_panel_index;
|
||||||
dock.panel_entries
|
let is_open = dock.is_open;
|
||||||
.iter()
|
|
||||||
.map(|panel| panel.panel.persistent_name()),
|
let buttons = dock
|
||||||
)
|
.panel_entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, panel)| {
|
||||||
|
let icon = panel.panel.icon(cx)?;
|
||||||
|
let name = panel.panel.persistent_name();
|
||||||
|
let action = panel.panel.toggle_action(cx);
|
||||||
|
let action2 = action.boxed_clone();
|
||||||
|
|
||||||
|
let mut button = IconButton::new(panel.panel.persistent_name(), icon)
|
||||||
|
.when(i == active_index, |el| el.state(InteractionState::Active))
|
||||||
|
.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
|
||||||
|
.tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
|
||||||
|
|
||||||
|
Some(button)
|
||||||
|
});
|
||||||
|
|
||||||
|
h_stack().children(buttons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -674,7 +692,7 @@ impl StatusItemView for PanelButtons {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{div, Div, ViewContext, WindowContext};
|
use gpui::{actions, div, Div, ViewContext, WindowContext};
|
||||||
|
|
||||||
pub struct TestPanel {
|
pub struct TestPanel {
|
||||||
pub position: DockPosition,
|
pub position: DockPosition,
|
||||||
|
@ -683,6 +701,7 @@ pub mod test {
|
||||||
pub has_focus: bool,
|
pub has_focus: bool,
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
}
|
}
|
||||||
|
actions!(ToggleTestPanel);
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TestPanel {}
|
impl EventEmitter<PanelEvent> for TestPanel {}
|
||||||
|
|
||||||
|
@ -732,12 +751,12 @@ pub mod test {
|
||||||
self.size = size.unwrap_or(300.);
|
self.size = size.unwrap_or(300.);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
|
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
|
||||||
Some("icons/test_panel.svg")
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
|
fn toggle_action(&self) -> Box<dyn Action> {
|
||||||
("Test Panel".into(), None)
|
ToggleTestPanel.boxed_clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
fn is_zoomed(&self, _: &WindowContext) -> bool {
|
||||||
|
|
|
@ -25,7 +25,7 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ui::v_stack;
|
use ui::v_stack;
|
||||||
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip};
|
use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
|
||||||
use util::truncate_and_remove_front;
|
use util::truncate_and_remove_front;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
|
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
|
||||||
|
@ -1392,7 +1392,7 @@ impl Pane {
|
||||||
.id(item.id())
|
.id(item.id())
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when_some(item.tab_tooltip_text(cx), |div, text| {
|
.when_some(item.tab_tooltip_text(cx), |div, text| {
|
||||||
div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
|
div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
|
||||||
})
|
})
|
||||||
.on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))
|
.on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))
|
||||||
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
|
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
|
||||||
|
|
|
@ -6,6 +6,7 @@ use gpui::{
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
|
use ui::h_stack;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
pub trait StatusItemView: Render {
|
pub trait StatusItemView: Render {
|
||||||
|
@ -53,16 +54,14 @@ impl Render for StatusBar {
|
||||||
|
|
||||||
impl StatusBar {
|
impl StatusBar {
|
||||||
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
||||||
div()
|
h_stack()
|
||||||
.flex()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.children(self.left_items.iter().map(|item| item.to_any()))
|
.children(self.left_items.iter().map(|item| item.to_any()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
||||||
div()
|
h_stack()
|
||||||
.flex()
|
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(self.right_items.iter().map(|item| item.to_any()))
|
.children(self.right_items.iter().map(|item| item.to_any()))
|
||||||
|
|
|
@ -29,18 +29,18 @@ use client2::{
|
||||||
Client, TypedEnvelope, UserStore,
|
Client, TypedEnvelope, UserStore,
|
||||||
};
|
};
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
|
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
future::try_join_all,
|
future::try_join_all,
|
||||||
Future, FutureExt, StreamExt,
|
Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
|
actions, div, point, prelude::*, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
|
||||||
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
|
AsyncAppContext, AsyncWindowContext, Bounds, Div, Entity, EntityId, EventEmitter, FocusHandle,
|
||||||
EventEmitter, FocusHandle, FocusableView, GlobalPixels, KeyContext, Model, ModelContext,
|
FocusableView, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
|
||||||
ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView,
|
Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
|
||||||
WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -68,8 +68,6 @@ use std::{
|
||||||
};
|
};
|
||||||
use theme2::{ActiveTheme, ThemeSettings};
|
use theme2::{ActiveTheme, ThemeSettings};
|
||||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||||
use ui::TextColor;
|
|
||||||
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
|
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
|
||||||
|
@ -440,7 +438,7 @@ pub struct Workspace {
|
||||||
last_active_view_id: Option<proto::ViewId>,
|
last_active_view_id: Option<proto::ViewId>,
|
||||||
status_bar: View<StatusBar>,
|
status_bar: View<StatusBar>,
|
||||||
modal_layer: View<ModalLayer>,
|
modal_layer: View<ModalLayer>,
|
||||||
// titlebar_item: Option<AnyViewHandle>,
|
titlebar_item: Option<AnyView>,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
follower_states: HashMap<View<Pane>, FollowerState>,
|
follower_states: HashMap<View<Pane>, FollowerState>,
|
||||||
|
@ -653,7 +651,7 @@ impl Workspace {
|
||||||
last_active_view_id: None,
|
last_active_view_id: None,
|
||||||
status_bar,
|
status_bar,
|
||||||
modal_layer,
|
modal_layer,
|
||||||
// titlebar_item: None,
|
titlebar_item: None,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
left_dock,
|
left_dock,
|
||||||
bottom_dock,
|
bottom_dock,
|
||||||
|
@ -1022,15 +1020,14 @@ impl Workspace {
|
||||||
&self.app_state.client
|
&self.app_state.client
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!()
|
pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
|
||||||
// pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
self.titlebar_item = Some(item);
|
||||||
// self.titlebar_item = Some(item);
|
cx.notify();
|
||||||
// cx.notify();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
|
pub fn titlebar_item(&self) -> Option<AnyView> {
|
||||||
// self.titlebar_item.clone()
|
self.titlebar_item.clone()
|
||||||
// }
|
}
|
||||||
|
|
||||||
/// Call the given callback with a workspace whose project is local.
|
/// Call the given callback with a workspace whose project is local.
|
||||||
///
|
///
|
||||||
|
@ -1592,52 +1589,52 @@ impl Workspace {
|
||||||
// .downcast()
|
// .downcast()
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /// Focus the panel of the given type if it isn't already focused. If it is
|
/// Focus the panel of the given type if it isn't already focused. If it is
|
||||||
// /// already focused, then transfer focus back to the workspace center.
|
/// already focused, then transfer focus back to the workspace center.
|
||||||
// pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
// self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /// Focus or unfocus the given panel type, depending on the given callback.
|
/// Focus or unfocus the given panel type, depending on the given callback.
|
||||||
// fn focus_or_unfocus_panel<T: Panel>(
|
fn focus_or_unfocus_panel<T: Panel>(
|
||||||
// &mut self,
|
&mut self,
|
||||||
// cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
// should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
||||||
// ) -> Option<Rc<dyn PanelHandle>> {
|
) -> Option<Arc<dyn PanelHandle>> {
|
||||||
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||||
// if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
|
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
|
||||||
// let mut focus_center = false;
|
let mut focus_center = false;
|
||||||
// let mut reveal_dock = false;
|
let mut reveal_dock = false;
|
||||||
// let panel = dock.update(cx, |dock, cx| {
|
let panel = dock.update(cx, |dock, cx| {
|
||||||
// dock.activate_panel(panel_index, cx);
|
dock.activate_panel(panel_index, cx);
|
||||||
|
|
||||||
// let panel = dock.active_panel().cloned();
|
let panel = dock.active_panel().cloned();
|
||||||
// if let Some(panel) = panel.as_ref() {
|
if let Some(panel) = panel.as_ref() {
|
||||||
// if should_focus(&**panel, cx) {
|
if should_focus(&**panel, cx) {
|
||||||
// dock.set_open(true, cx);
|
dock.set_open(true, cx);
|
||||||
// cx.focus(panel.as_any());
|
panel.focus_handle(cx).focus(cx);
|
||||||
// reveal_dock = true;
|
reveal_dock = true;
|
||||||
// } else {
|
} else {
|
||||||
// // if panel.is_zoomed(cx) {
|
// if panel.is_zoomed(cx) {
|
||||||
// // dock.set_open(false, cx);
|
// dock.set_open(false, cx);
|
||||||
// // }
|
// }
|
||||||
// focus_center = true;
|
focus_center = true;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// panel
|
panel
|
||||||
// });
|
});
|
||||||
|
|
||||||
// if focus_center {
|
if focus_center {
|
||||||
// cx.focus_self();
|
self.active_pane.update(cx, |pane, cx| pane.focus(cx))
|
||||||
// }
|
}
|
||||||
|
|
||||||
// self.serialize_workspace(cx);
|
self.serialize_workspace(cx);
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// return panel;
|
return panel;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// None
|
None
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
|
// pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
|
||||||
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
// for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||||
|
@ -2437,75 +2434,6 @@ impl Workspace {
|
||||||
// .any(|state| state.leader_id == peer_id)
|
// .any(|state| state.leader_id == peer_id)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
fn render_titlebar(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
|
|
||||||
h_stack()
|
|
||||||
.id("titlebar")
|
|
||||||
.justify_between()
|
|
||||||
.when(
|
|
||||||
!matches!(cx.window_bounds(), WindowBounds::Fullscreen),
|
|
||||||
|s| s.pl_20(),
|
|
||||||
)
|
|
||||||
.w_full()
|
|
||||||
.h(rems(1.75))
|
|
||||||
.bg(cx.theme().colors().title_bar_background)
|
|
||||||
.on_click(|_, event, cx| {
|
|
||||||
if event.up.click_count == 2 {
|
|
||||||
cx.zoom_window();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_stack()
|
|
||||||
// TODO - Add player menu
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("project_owner_indicator")
|
|
||||||
.child(
|
|
||||||
Button::new("player")
|
|
||||||
.variant(ButtonVariant::Ghost)
|
|
||||||
.color(Some(TextColor::Player(0))),
|
|
||||||
)
|
|
||||||
.tooltip(move |_, cx| {
|
|
||||||
cx.build_view(|cx| TextTooltip::new("Toggle following"))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
// TODO - Add project menu
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("titlebar_project_menu_button")
|
|
||||||
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
|
|
||||||
.tooltip(move |_, cx| {
|
|
||||||
cx.build_view(|cx| TextTooltip::new("Recent Projects"))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
// TODO - Add git menu
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("titlebar_git_menu_button")
|
|
||||||
.child(
|
|
||||||
Button::new("branch_name")
|
|
||||||
.variant(ButtonVariant::Ghost)
|
|
||||||
.color(Some(TextColor::Muted)),
|
|
||||||
)
|
|
||||||
.tooltip(move |_, cx| {
|
|
||||||
// todo!() Replace with real action.
|
|
||||||
#[gpui::action]
|
|
||||||
struct NoAction {}
|
|
||||||
|
|
||||||
cx.build_view(|cx| {
|
|
||||||
TextTooltip::new("Recent Branches")
|
|
||||||
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
|
|
||||||
"cmd-b",
|
|
||||||
NoAction {},
|
|
||||||
None,
|
|
||||||
)))
|
|
||||||
.meta("Only local branches shown")
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
) // self.titlebar_item
|
|
||||||
.child(h_stack().child(Label::new("Right side titlebar item")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
|
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let active_entry = self.active_project_path(cx);
|
let active_entry = self.active_project_path(cx);
|
||||||
self.project
|
self.project
|
||||||
|
@ -3701,7 +3629,7 @@ impl Render for Workspace {
|
||||||
.items_start()
|
.items_start()
|
||||||
.text_color(cx.theme().colors().text)
|
.text_color(cx.theme().colors().text)
|
||||||
.bg(cx.theme().colors().background)
|
.bg(cx.theme().colors().background)
|
||||||
.child(self.render_titlebar(cx))
|
.children(self.titlebar_item.clone())
|
||||||
.child(
|
.child(
|
||||||
// todo! should this be a component a view?
|
// todo! should this be a component a view?
|
||||||
div()
|
div()
|
||||||
|
|
|
@ -23,7 +23,7 @@ ai = { package = "ai2", path = "../ai2"}
|
||||||
call = { package = "call2", path = "../call2" }
|
call = { package = "call2", path = "../call2" }
|
||||||
# channel = { path = "../channel" }
|
# channel = { path = "../channel" }
|
||||||
cli = { path = "../cli" }
|
cli = { path = "../cli" }
|
||||||
# collab_ui = { path = "../collab_ui" }
|
collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
command_palette = { package="command_palette2", path = "../command_palette2" }
|
command_palette = { package="command_palette2", path = "../command_palette2" }
|
||||||
# component_test = { path = "../component_test" }
|
# component_test = { path = "../component_test" }
|
||||||
|
|
|
@ -206,7 +206,7 @@ fn main() {
|
||||||
// activity_indicator::init(cx);
|
// activity_indicator::init(cx);
|
||||||
// language_tools::init(cx);
|
// language_tools::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
// collab_ui::init(&app_state, cx);
|
collab_ui::init(&app_state, cx);
|
||||||
// feedback::init(cx);
|
// feedback::init(cx);
|
||||||
// welcome::init(cx);
|
// welcome::init(cx);
|
||||||
// zed::init(&app_state, cx);
|
// zed::init(&app_state, cx);
|
||||||
|
|
|
@ -176,8 +176,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
// let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
// let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
// let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
// let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
// let channels_panel =
|
let channels_panel =
|
||||||
// collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
// let chat_panel =
|
// let chat_panel =
|
||||||
// collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
|
// collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
|
// let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
|
||||||
|
@ -188,14 +188,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
project_panel,
|
project_panel,
|
||||||
// terminal_panel,
|
// terminal_panel,
|
||||||
// assistant_panel,
|
// assistant_panel,
|
||||||
// channels_panel,
|
channels_panel,
|
||||||
// chat_panel,
|
// chat_panel,
|
||||||
// notification_panel,
|
// notification_panel,
|
||||||
) = futures::try_join!(
|
) = futures::try_join!(
|
||||||
project_panel,
|
project_panel,
|
||||||
// terminal_panel,
|
// terminal_panel,
|
||||||
// assistant_panel,
|
// assistant_panel,
|
||||||
// channels_panel,
|
channels_panel,
|
||||||
// chat_panel,
|
// chat_panel,
|
||||||
// notification_panel,
|
// notification_panel,
|
||||||
)?;
|
)?;
|
||||||
|
@ -205,7 +205,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
workspace.add_panel(project_panel, cx);
|
workspace.add_panel(project_panel, cx);
|
||||||
// workspace.add_panel(terminal_panel, cx);
|
// workspace.add_panel(terminal_panel, cx);
|
||||||
// workspace.add_panel(assistant_panel, cx);
|
// workspace.add_panel(assistant_panel, cx);
|
||||||
// workspace.add_panel(channels_panel, cx);
|
workspace.add_panel(channels_panel, cx);
|
||||||
// workspace.add_panel(chat_panel, cx);
|
// workspace.add_panel(chat_panel, cx);
|
||||||
// workspace.add_panel(notification_panel, cx);
|
// workspace.add_panel(notification_panel, cx);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue