Detailed changes
@@ -1762,7 +1762,7 @@ dependencies = [
"anyhow",
"async-trait",
"async-tungstenite",
- "audio2",
+ "audio",
"axum",
"axum-extra",
"base64 0.13.1",
@@ -1771,7 +1771,7 @@ dependencies = [
"clap 3.2.25",
"client2",
"clock",
- "collab_ui2",
+ "collab_ui",
"collections",
"ctor",
"dashmap",
@@ -1831,53 +1831,6 @@ dependencies = [
[[package]]
name = "collab_ui"
version = "0.1.0"
-dependencies = [
- "anyhow",
- "auto_update",
- "call",
- "channel",
- "client",
- "clock",
- "collections",
- "context_menu",
- "db",
- "drag_and_drop",
- "editor",
- "feature_flags",
- "feedback",
- "futures 0.3.28",
- "fuzzy",
- "gpui",
- "language",
- "lazy_static",
- "log",
- "menu",
- "notifications",
- "picker",
- "postage",
- "pretty_assertions",
- "project",
- "recent_projects",
- "rich_text",
- "rpc",
- "schemars",
- "serde",
- "serde_derive",
- "settings",
- "smallvec",
- "theme",
- "theme_selector",
- "time",
- "tree-sitter-markdown",
- "util",
- "vcs_menu",
- "workspace",
- "zed-actions",
-]
-
-[[package]]
-name = "collab_ui2"
-version = "0.1.0"
dependencies = [
"anyhow",
"auto_update2",
@@ -1916,7 +1869,7 @@ dependencies = [
"tree-sitter-markdown",
"ui2",
"util",
- "vcs_menu2",
+ "vcs_menu",
"workspace2",
"zed_actions2",
]
@@ -7010,7 +6963,7 @@ dependencies = [
]
[[package]]
-name = "quick_action_bar2"
+name = "quick_action_bar"
version = "0.1.0"
dependencies = [
"assistant2",
@@ -10604,20 +10557,6 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vcs_menu"
version = "0.1.0"
-dependencies = [
- "anyhow",
- "fs",
- "fuzzy",
- "gpui",
- "picker",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "vcs_menu2"
-version = "0.1.0"
dependencies = [
"anyhow",
"fs2",
@@ -11093,31 +11032,6 @@ checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "welcome"
version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "db",
- "editor",
- "fs",
- "fuzzy",
- "gpui",
- "install_cli",
- "log",
- "picker",
- "project",
- "schemars",
- "serde",
- "settings",
- "theme",
- "theme_selector",
- "util",
- "vim",
- "workspace",
-]
-
-[[package]]
-name = "welcome2"
-version = "0.1.0"
dependencies = [
"anyhow",
"client2",
@@ -11557,7 +11471,7 @@ dependencies = [
"chrono",
"cli",
"client2",
- "collab_ui2",
+ "collab_ui",
"collections",
"command_palette2",
"copilot2",
@@ -11598,7 +11512,7 @@ dependencies = [
"project2",
"project_panel2",
"project_symbols2",
- "quick_action_bar2",
+ "quick_action_bar",
"rand 0.8.5",
"recent_projects2",
"regex",
@@ -11661,7 +11575,7 @@ dependencies = [
"util",
"uuid 1.4.1",
"vim2",
- "welcome2",
+ "welcome",
"workspace2",
"zed_actions2",
]
@@ -22,7 +22,6 @@ members = [
"crates/collab",
"crates/collab2",
"crates/collab_ui",
- "crates/collab_ui2",
"crates/collections",
"crates/command_palette",
"crates/command_palette2",
@@ -91,7 +90,7 @@ members = [
"crates/project_panel2",
"crates/project_symbols",
"crates/project_symbols2",
- "crates/quick_action_bar2",
+ "crates/quick_action_bar",
"crates/recent_projects",
"crates/recent_projects2",
"crates/rope",
@@ -123,10 +122,8 @@ members = [
"crates/story",
"crates/vim",
"crates/vcs_menu",
- "crates/vcs_menu2",
"crates/workspace2",
"crates/welcome",
- "crates/welcome2",
"crates/xtask",
"crates/zed",
"crates/zed-actions",
@@ -60,7 +60,7 @@ tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
uuid.workspace = true
[dev-dependencies]
-audio = { package = "audio2", path = "../audio2" }
+audio = { path = "../audio" }
collections = { path = "../collections", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
call = { package = "call2", path = "../call2", features = ["test-support"] }
@@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
-collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
+collab_ui = { path = "../collab_ui", features = ["test-support"] }
async-trait.workspace = true
pretty_assertions.workspace = true
@@ -22,35 +22,36 @@ test-support = [
]
[dependencies]
-auto_update = { path = "../auto_update" }
-db = { path = "../db" }
-call = { path = "../call" }
-client = { path = "../client" }
-channel = { path = "../channel" }
+auto_update = { package = "auto_update2", path = "../auto_update2" }
+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 = { path = "../editor" }
-feedback = { path = "../feedback" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-menu = { path = "../menu" }
-notifications = { path = "../notifications" }
-rich_text = { path = "../rich_text" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-recent_projects = { path = "../recent_projects" }
-rpc = { path = "../rpc" }
-settings = { path = "../settings" }
-feature_flags = {path = "../feature_flags"}
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+# context_menu = { path = "../context_menu" }
+# drag_and_drop = { path = "../drag_and_drop" }
+editor = { package="editor2", path = "../editor2" }
+feedback = { package = "feedback2", path = "../feedback2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+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 = { package = "recent_projects2", path = "../recent_projects2" }
+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 = { package = "theme_selector2", path = "../theme_selector2" }
vcs_menu = { path = "../vcs_menu" }
+ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
-workspace = { path = "../workspace" }
-zed-actions = {path = "../zed-actions"}
+workspace = { package = "workspace2", path = "../workspace2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true
futures.workspace = true
@@ -64,17 +65,17 @@ time.workspace = true
smallvec.workspace = true
[dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
+call = { package = "call2", path = "../call2", features = ["test-support"] }
+client = { package = "client2", path = "../client2", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-notifications = { path = "../notifications", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", 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 = { path = "../workspace", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
pretty_assertions.workspace = true
tree-sitter-markdown.workspace = true
@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::Result;
use call::report_call_event_for_channel;
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
use client::{
@@ -6,20 +6,18 @@ use client::{
Collaborator, ParticipantIndex,
};
use collections::HashMap;
-use editor::{CollaborationHub, Editor};
+use editor::{CollaborationHub, Editor, EditorEvent};
use gpui::{
- actions,
- elements::{ChildView, Label},
- geometry::vector::Vector2F,
- AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
- ViewContext, ViewHandle,
+ actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
+ IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
+ VisualContext as _, WindowContext,
};
use project::Project;
-use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
sync::Arc,
};
+use ui::{prelude::*, Label};
use util::ResultExt;
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle},
@@ -28,17 +26,17 @@ use workspace::{
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
};
-actions!(channel_view, [Deploy]);
+actions!(collab, [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>,
+ pub editor: View<Editor>,
+ project: Model<Project>,
+ channel_store: Model<ChannelStore>,
+ channel_buffer: Model<ChannelBuffer>,
remote_id: Option<ViewId>,
_editor_event_subscription: Subscription,
}
@@ -46,9 +44,9 @@ pub struct ChannelView {
impl ChannelView {
pub fn open(
channel_id: ChannelId,
- workspace: ViewHandle<Workspace>,
- cx: &mut AppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
+ workspace: View<Workspace>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<View<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 {
@@ -61,17 +59,17 @@ impl ChannelView {
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>>> {
+ pane: View<Pane>,
+ workspace: View<Workspace>,
+ cx: &mut WindowContext,
+ ) -> Task<Result<View<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = ChannelStore::global(cx);
@@ -91,7 +89,7 @@ impl ChannelView {
buffer.set_language(Some(markdown), cx);
}
})
- });
+ })?;
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_buffer.read(cx).remote_id(cx);
@@ -107,7 +105,7 @@ impl ChannelView {
}
}
- let view = cx.add_view(|cx| {
+ let view = cx.new_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
@@ -117,7 +115,7 @@ impl ChannelView {
// 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)
+ pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
@@ -125,18 +123,17 @@ impl ChannelView {
view
})
- .ok_or_else(|| anyhow!("pane was dropped"))
})
}
pub fn new(
- project: ModelHandle<Project>,
- channel_store: ModelHandle<ChannelStore>,
- channel_buffer: ModelHandle<ChannelBuffer>,
+ project: Model<Project>,
+ channel_store: Model<ChannelStore>,
+ channel_buffer: Model<ChannelBuffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
- let editor = cx.add_view(|cx| {
+ let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
@@ -149,7 +146,8 @@ impl ChannelView {
);
editor
});
- let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+ let _editor_event_subscription =
+ cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
.detach();
@@ -170,7 +168,7 @@ impl ChannelView {
fn handle_channel_buffer_event(
&mut self,
- _: ModelHandle<ChannelBuffer>,
+ _: Model<ChannelBuffer>,
event: &ChannelBufferEvent,
cx: &mut ViewContext<Self>,
) {
@@ -182,12 +180,12 @@ impl ChannelView {
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.emit(editor::EditorEvent::TitleChanged);
cx.notify()
});
}
ChannelBufferEvent::BufferEdited => {
- if cx.is_self_focused() || self.editor.is_focused(cx) {
+ if self.editor.read(cx).is_focused(cx) {
self.acknowledge_buffer_version(cx);
} else {
self.channel_store.update(cx, |store, cx| {
@@ -205,7 +203,7 @@ impl ChannelView {
}
}
- fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+ 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(
@@ -221,49 +219,39 @@ impl ChannelView {
}
}
-impl Entity for ChannelView {
- type Event = editor::Event;
-}
-
-impl View for ChannelView {
- fn ui_name() -> &'static str {
- "ChannelView"
- }
+impl EventEmitter<EditorEvent> for ChannelView {}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- ChildView::new(self.editor.as_any(), cx).into_any()
+impl Render for ChannelView {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ self.editor.clone()
}
+}
- 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 FocusableView for ChannelView {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.editor.read(cx).focus_handle(cx)
}
}
impl Item for ChannelView {
+ type Event = EditorEvent;
+
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
- self_handle: &'a ViewHandle<Self>,
+ self_handle: &'a View<Self>,
_: &'a AppContext,
- ) -> Option<&'a AnyViewHandle> {
+ ) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
- Some(self_handle)
+ Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
- Some(&self.editor)
+ Some(self.editor.to_any())
} else {
None
}
}
- fn tab_content<V: 'static>(
- &self,
- _: Option<usize>,
- style: &theme::Tab,
- cx: &gpui::AppContext,
- ) -> AnyElement<V> {
+ fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
let label = if let Some(channel) = self.channel(cx) {
match (
channel.can_edit_notes(),
@@ -276,16 +264,24 @@ impl Item for ChannelView {
} else {
format!("channel notes (disconnected)")
};
- Label::new(label, style.label.to_owned()).into_any()
+ Label::new(label)
+ .color(if selected {
+ Color::Default
+ } else {
+ Color::Muted
+ })
+ .into_any_element()
}
- 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 clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
+ Some(cx.new_view(|cx| {
+ Self::new(
+ self.project.clone(),
+ self.channel_store.clone(),
+ self.channel_buffer.clone(),
+ cx,
+ )
+ }))
}
fn is_singleton(&self, _cx: &AppContext) -> bool {
@@ -307,7 +303,7 @@ impl Item for ChannelView {
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
}
- fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -315,12 +311,12 @@ impl Item for ChannelView {
true
}
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+ fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
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)
+ fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+ Editor::to_item_events(event, f)
}
}
@@ -329,7 +325,7 @@ impl FollowableItem for ChannelView {
self.remote_id
}
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
@@ -350,12 +346,12 @@ impl FollowableItem for ChannelView {
}
fn from_state_proto(
- pane: ViewHandle<workspace::Pane>,
- workspace: ViewHandle<workspace::Workspace>,
+ pane: View<workspace::Pane>,
+ workspace: View<workspace::Workspace>,
remote_id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
- ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+ cx: &mut WindowContext,
+ ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
let Some(proto::view::Variant::ChannelView(_)) = state else {
return None;
};
@@ -368,30 +364,28 @@ impl FollowableItem for ChannelView {
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"))?;
+ 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
+ }
+ })?;
if let Some(task) = task {
task.await?;
@@ -403,9 +397,9 @@ impl FollowableItem for ChannelView {
fn add_event_to_update_proto(
&self,
- event: &Self::Event,
+ event: &EditorEvent,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool {
self.editor
.read(cx)
@@ -414,7 +408,7 @@ impl FollowableItem for ChannelView {
fn apply_update_proto(
&mut self,
- project: &ModelHandle<Project>,
+ project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> gpui::Task<anyhow::Result<()>> {
@@ -429,16 +423,16 @@ impl FollowableItem for ChannelView {
})
}
- fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
- Editor::should_unfollow_on_event(event, cx)
+ fn is_project_item(&self, _cx: &WindowContext) -> bool {
+ false
}
- fn is_project_item(&self, _cx: &AppContext) -> bool {
- false
+ fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
+ Editor::to_follow_event(event)
}
}
-struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
impl CollaborationHub for ChannelBufferCollaborationHub {
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
@@ -1,6 +1,4 @@
-use crate::{
- channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-};
+use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
use anyhow::Result;
use call::ActiveCall;
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -9,13 +7,9 @@ use 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,
+ actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+ ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
+ Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
};
use language::LanguageRegistry;
use menu::Confirm;
@@ -23,13 +17,14 @@ use message_editor::MessageEditor;
use project::Fs;
use rich_text::RichText;
use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore};
use std::sync::Arc;
-use theme::{IconButton, Theme};
+use theme::ActiveTheme as _;
use time::{OffsetDateTime, UtcOffset};
+use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::{
- dock::{DockPosition, Panel},
+ dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
@@ -38,29 +33,36 @@ mod message_editor;
const MESSAGE_LOADING_THRESHOLD: usize = 50;
const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::<ChatPanel>(cx);
+ });
+ })
+ .detach();
+}
+
pub struct ChatPanel {
client: Arc<Client>,
- channel_store: ModelHandle<ChannelStore>,
+ channel_store: Model<ChannelStore>,
languages: Arc<LanguageRegistry>,
- active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
- message_list: ListState<ChatPanel>,
- input_editor: ViewHandle<MessageEditor>,
- channel_select: ViewHandle<Select>,
+ message_list: ListState,
+ active_chat: Option<(Model<ChannelChat>, Subscription)>,
+ input_editor: View<MessageEditor>,
local_timezone: UtcOffset,
fs: Arc<dyn Fs>,
- width: Option<f32>,
+ width: Option<Pixels>,
active: bool,
pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
- workspace: WeakViewHandle<Workspace>,
+ workspace: WeakView<Workspace>,
is_scrolled_to_bottom: bool,
- has_focus: bool,
markdown_data: HashMap<ChannelMessageId, RichText>,
}
#[derive(Serialize, Deserialize)]
struct SerializedChatPanel {
- width: Option<f32>,
+ width: Option<Pixels>,
}
#[derive(Debug)]
@@ -70,90 +72,56 @@ pub enum Event {
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);
-}
+actions!(chat_panel, [ToggleFocus]);
impl ChatPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+ pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
- let input_editor = cx.add_view(|cx| {
+ let input_editor = cx.new_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.new_view(|cx| Editor::auto_height(4, cx)),
cx,
)
});
let workspace_handle = workspace.weak_handle();
- let channel_select = cx.add_view(|cx| {
- let channel_store = channel_store.clone();
- let workspace = workspace_handle.clone();
- Select::new(0, cx, {
- move |ix, item_type, is_hovered, cx| {
- Self::render_channel_name(
- &channel_store,
- ix,
- item_type,
- is_hovered,
- workspace,
- cx,
- )
- }
- })
- .with_style(move |cx| {
- let style = &theme::current(cx).chat_panel.channel_select;
- SelectStyle {
- header: Default::default(),
- menu: style.menu,
- }
- })
- });
+ cx.new_view(|cx: &mut ViewContext<Self>| {
+ let view = cx.view().downgrade();
+ let message_list =
+ ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
+ if let Some(view) = view.upgrade() {
+ view.update(cx, |view, cx| {
+ view.render_message(ix, cx).into_any_element()
+ })
+ } else {
+ div().into_any()
+ }
+ });
- 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;
- });
+ message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
+ if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+ this.load_more_messages(cx);
+ }
+ this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+ }));
- cx.add_view(|cx| {
let mut this = Self {
fs,
client,
channel_store,
languages,
+ message_list,
active_chat: Default::default(),
pending_serialization: Task::ready(None),
- message_list,
input_editor,
- channel_select,
- local_timezone: cx.platform().local_timezone(),
- has_focus: false,
+ local_timezone: cx.local_timezone(),
subscriptions: Vec::new(),
workspace: workspace_handle,
is_scrolled_to_bottom: true,
@@ -163,38 +131,16 @@ impl ChatPanel {
};
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.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
})
@@ -204,17 +150,17 @@ impl ChatPanel {
self.is_scrolled_to_bottom
}
- pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+ pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
self.active_chat.as_ref().map(|(chat, _)| chat.clone())
}
pub fn load(
- workspace: WeakViewHandle<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
+ workspace: WeakView<Workspace>,
+ cx: AsyncWindowContext,
+ ) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
- .background()
+ .background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
.await
.log_err()
@@ -240,7 +186,7 @@ impl ChatPanel {
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
- self.pending_serialization = cx.background().spawn(
+ self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
@@ -254,14 +200,7 @@ impl ChatPanel {
);
}
- 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>) {
+ fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
let channel_id = chat.read(cx).channel_id;
{
@@ -277,18 +216,13 @@ impl ChatPanel {
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>,
+ _: Model<ChannelChat>,
event: &ChannelChatEvent,
cx: &mut ViewContext<Self>,
) {
@@ -316,7 +250,7 @@ impl ChatPanel {
cx.notify();
}
- fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
+ fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
if self.active && self.is_scrolled_to_bottom {
if let Some((chat, _)) = &self.active_chat {
chat.update(cx, |chat, cx| {
@@ -326,39 +260,60 @@ impl ChatPanel {
}
}
- 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),
+ fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ v_stack()
+ .full()
+ .on_action(cx.listener(Self::send))
+ .child(
+ h_stack().z_index(1).child(
+ TabBar::new("chat_header")
+ .child(
+ h_stack()
+ .w_full()
+ .h(rems(ui::Tab::HEIGHT_IN_REMS))
+ .px_2()
+ .child(Label::new(
+ self.active_chat
+ .as_ref()
+ .and_then(|c| {
+ Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
+ })
+ .unwrap_or_default(),
+ )),
+ )
+ .end_child(
+ IconButton::new("notes", Icon::File)
+ .on_click(cx.listener(Self::open_notes))
+ .tooltip(|cx| Tooltip::text("Open notes", cx)),
+ )
+ .end_child(
+ IconButton::new("call", Icon::AudioOn)
+ .on_click(cx.listener(Self::join_call))
+ .tooltip(|cx| Tooltip::text("Join call", cx)),
+ ),
+ ),
+ )
+ .child(div().flex_grow().px_2().py_1().map(|this| {
+ if self.active_chat.is_some() {
+ this.child(list(self.message_list.clone()).full())
+ } else {
+ this
+ }
+ }))
+ .child(
+ div()
+ .z_index(1)
+ .p_2()
+ .bg(cx.theme().colors().background)
+ .child(self.input_editor.clone()),
)
- .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| {
+ fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let active_chat = &self.active_chat.as_ref().unwrap().0;
+ let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
+ active_chat.update(cx, |active_chat, cx| {
let is_admin = self
.channel_store
.read(cx)
@@ -366,8 +321,13 @@ impl ChatPanel {
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;
+ let next_message =
+ active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
+
+ let is_continuation_from_previous = last_message.id != this_message.id
+ && last_message.sender.id == this_message.sender.id;
+ let is_continuation_to_next = this_message.id != next_message.id
+ && this_message.sender.id == next_message.sender.id;
if let ChannelMessageId::Saved(id) = this_message.id {
if this_message
@@ -381,28 +341,19 @@ impl ChatPanel {
(
this_message,
- is_continuation,
- active_chat.message_count() == ix + 1,
+ is_continuation_from_previous,
+ is_continuation_to_next,
is_admin,
)
});
- let is_pending = message.is_pending();
- let theme = theme::current(cx);
+ let _is_pending = message.is_pending();
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
});
let now = OffsetDateTime::now_utc();
- let style = if is_pending {
- &theme.chat_panel.pending_message
- } else if is_continuation {
- &theme.chat_panel.continuation_message
- } else {
- &theme.chat_panel.message
- };
-
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin)
@@ -412,89 +363,52 @@ impl ChatPanel {
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,
+ let element_id: ElementId = match message.id {
+ ChannelMessageId::Saved(id) => ("saved-message", id).into(),
+ ChannelMessageId::Pending(id) => ("pending-message", id).into(),
+ };
+
+ v_stack()
+ .w_full()
+ .id(element_id)
+ .relative()
+ .overflow_hidden()
+ .group("")
+ .when(!is_continuation_from_previous, |this| {
+ this.child(
+ h_stack()
+ .gap_2()
+ .child(Avatar::new(message.sender.avatar_uri.clone()))
+ .child(Label::new(message.sender.github_login.clone()))
+ .child(
+ Label::new(format_timestamp(
+ message.timestamp,
+ now,
+ self.local_timezone,
+ ))
+ .color(Color::Muted),
+ ),
+ )
+ })
+ .when(!is_continuation_to_next, |this|
+ // HACK: This should really be a margin, but margins seem to get collapsed.
+ this.pb_2())
+ .child(text.element("body".into(), cx))
+ .child(
+ div()
+ .absolute()
+ .top_1()
+ .right_2()
+ .w_8()
+ .visible_on_hover("")
+ .children(message_id_to_remove.map(|message_id| {
+ IconButton::new(("remove", message_id), Icon::XCircle).on_click(
+ cx.listener(move |this, _, cx| {
+ this.remove_message(message_id, 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(
@@ -514,127 +428,26 @@ impl ChatPanel {
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();
+ fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+ Button::new("sign-in", "Sign in to use chat")
+ .on_click(cx.listener(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, |_, cx| {
+ cx.focus_self();
+ })
+ .ok();
}
})
- .with_tooltip::<OpenChannelNotes>(
- channel_id as usize,
- "Open Notes",
- Some(Box::new(OpenChannelNotes)),
- tooltip_style.clone(),
- cx,
- )
- .flex_float(),
- MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
- render_icon_button(
- theme.icon_button.style_for(mouse_state),
- "icons/speaker-loud.svg",
- )
- })
- .on_click(MouseButton::Left, move |_, _, cx| {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.join_channel(channel_id, cx))
- .detach_and_log_err(cx);
- })
- .with_tooltip::<ActiveCall>(
- channel_id as usize,
- "Join Call",
- Some(Box::new(JoinCall)),
- tooltip_style.clone(),
- cx,
- )
- .flex_float(),
- ]);
- }
-
- row.align_children_center()
- .contained()
- .with_style(style.container)
- .into_any()
- }
-
- fn render_sign_in_prompt(
- &self,
- theme: &Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum SignInPromptLabel {}
-
- MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
- Label::new(
- "Sign in to use chat".to_string(),
- theme
- .chat_panel
- .sign_in_prompt
- .style_for(mouse_state)
- .clone(),
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let client = this.client.clone();
- cx.spawn(|this, mut cx| async move {
- if client
- .authenticate_and_connect(true, &cx)
- .log_err()
- .await
- .is_some()
- {
- this.update(&mut cx, |this, cx| {
- if cx.handle().is_focused(cx) {
- cx.focus(&this.input_editor);
- }
- })
- .ok();
- }
- })
- .detach();
- })
- .aligned()
- .into_any()
+ .detach();
+ }))
+ .into_any_element()
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@@ -658,7 +471,7 @@ impl ChatPanel {
}
}
- fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+ fn load_more_messages(&mut self, 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) {
@@ -695,14 +508,14 @@ impl ChatPanel {
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())
+ 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.,
+ offset_in_item: px(0.0),
});
cx.notify();
}
@@ -714,16 +527,16 @@ impl ChatPanel {
})
}
- fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+ fn open_notes(&mut self, _: &ClickEvent, 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) {
+ if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
- fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+ fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel_id;
ActiveCall::global(cx)
@@ -733,89 +546,30 @@ impl ChatPanel {
}
}
-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 EventEmitter<Event> for ChatPanel {}
-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);
- }
+impl Render for ChatPanel {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ div()
+ .full()
+ .child(if self.client.user_id().is_some() {
+ self.render_channel(cx)
+ } else {
+ self.render_sign_in_prompt(cx)
+ })
+ .min_w(px(150.))
}
+}
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
+impl FocusableView for ChatPanel {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.input_editor.read(cx).focus_handle(cx)
}
}
impl Panel for ChatPanel {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- settings::get::<ChatPanelSettings>(cx).dock
+ ChatPanelSettings::get_global(cx).dock
}
fn position_is_valid(&self, position: DockPosition) -> bool {
@@ -828,12 +582,12 @@ impl Panel for ChatPanel {
});
}
- fn size(&self, cx: &gpui::WindowContext) -> f32 {
+ fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
- .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+ .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
}
- fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+ fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
@@ -849,32 +603,25 @@ impl Panel for ChatPanel {
}
}
- 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 persistent_name() -> &'static str {
+ "ChatPanel"
}
- fn should_close_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::Dismissed)
+ fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
+ Some(ui::Icon::MessageBubbles)
}
- fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
- self.has_focus
+ fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+ Some("Chat Panel")
}
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, Event::Focus)
+ fn toggle_action(&self) -> Box<dyn gpui::Action> {
+ Box::new(ToggleFocus)
}
}
+impl EventEmitter<PanelEvent> for ChatPanel {}
+
fn format_timestamp(
mut timestamp: OffsetDateTime,
mut now: OffsetDateTime,
@@ -900,25 +647,12 @@ fn format_timestamp(
}
}
-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 gpui::HighlightStyle;
use pretty_assertions::assert_eq;
- use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+ use rich_text::Highlight;
use util::test::marked_text_ranges;
#[gpui::test]
@@ -931,7 +665,7 @@ mod tests {
timestamp: OffsetDateTime::now_utc(),
sender: Arc::new(client::User {
github_login: "fgh".into(),
- avatar: None,
+ avatar_uri: "avatar_fgh".into(),
id: 103,
}),
nonce: 5,
@@ -949,7 +683,7 @@ mod tests {
(
ranges[0].clone(),
HighlightStyle {
- italic: Some(true),
+ font_style: Some(gpui::FontStyle::Italic),
..Default::default()
}
.into()
@@ -3,13 +3,14 @@ use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
- elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
+ ViewContext, WeakView,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
+use workspace::item::ItemHandle;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@@ -19,8 +20,8 @@ lazy_static! {
}
pub struct MessageEditor {
- pub editor: ViewHandle<Editor>,
- channel_store: ModelHandle<ChannelStore>,
+ pub editor: View<Editor>,
+ channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
@@ -30,8 +31,8 @@ pub struct MessageEditor {
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
- channel_store: ModelHandle<ChannelStore>,
- editor: ViewHandle<Editor>,
+ channel_store: Model<ChannelStore>,
+ editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
@@ -48,15 +49,13 @@ impl MessageEditor {
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(())
+ cx.spawn(|_, mut cx| async move {
+ let markdown = markdown.await?;
+ buffer.update(&mut cx, |buffer, cx| {
+ buffer.set_language(Some(markdown), cx)
})
- .detach_and_log_err(cx);
+ })
+ .detach_and_log_err(cx);
Self {
editor,
@@ -71,7 +70,7 @@ impl MessageEditor {
pub fn set_channel(
&mut self,
channel_id: u64,
- channel_name: Option<String>,
+ channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
@@ -132,26 +131,28 @@ impl MessageEditor {
fn on_buffer_event(
&mut self,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<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;
+ cx.background_executor()
+ .timer(MENTIONS_DEBOUNCE_INTERVAL)
+ .await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
- this: WeakViewHandle<MessageEditor>,
+ this: WeakView<MessageEditor>,
buffer: BufferSnapshot,
- mut cx: AsyncAppContext,
+ mut cx: AsyncWindowContext,
) {
let (buffer, ranges) = cx
- .background()
+ .background_executor()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
@@ -180,11 +181,7 @@ impl MessageEditor {
}
editor.clear_highlights::<Self>(cx);
- editor.highlight_text::<Self>(
- anchor_ranges,
- theme::current(cx).chat_panel.rich_text.mention_highlight,
- cx,
- )
+ editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
});
this.mentions = mentioned_user_ids;
@@ -192,21 +189,15 @@ impl MessageEditor {
})
.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()
+ pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
+ self.editor.read(cx).focus_handle(cx)
}
+}
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- cx.focus(&self.editor);
- }
+impl Render for MessageEditor {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ self.editor.to_any()
}
}
@@ -214,7 +205,7 @@ impl View for MessageEditor {
mod tests {
use super::*;
use client::{Client, User, UserStore};
- use gpui::{TestAppContext, WindowHandle};
+ use gpui::{Context as _, TestAppContext, VisualContext as _};
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;
@@ -222,8 +213,17 @@ mod tests {
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
- let editor = init_test(cx);
- let editor = editor.root(cx);
+ let language_registry = init_test(cx);
+
+ let (editor, cx) = cx.add_window_view(|cx| {
+ MessageEditor::new(
+ language_registry,
+ ChannelStore::global(cx),
+ cx.new_view(|cx| Editor::auto_height(4, cx)),
+ cx,
+ )
+ });
+ cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
editor.set_members(
@@ -232,7 +232,7 @@ mod tests {
user: Arc::new(User {
github_login: "a-b".into(),
id: 101,
- avatar: None,
+ avatar_uri: "avatar_a-b".into(),
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
@@ -241,7 +241,7 @@ mod tests {
user: Arc::new(User {
github_login: "C_D".into(),
id: 102,
- avatar: None,
+ avatar_uri: "avatar_C_D".into(),
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
@@ -255,7 +255,7 @@ mod tests {
});
});
- cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+ cx.executor().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);
@@ -269,15 +269,14 @@ mod tests {
});
}
- fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
- cx.foreground().forbid_parking();
-
+ fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
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);
+ let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
@@ -292,16 +291,6 @@ mod tests {
},
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
+ language_registry
}
}
@@ -1,123 +1,46 @@
mod channel_modal;
mod contact_finder;
+use self::channel_modal::ChannelModal;
use crate::{
- channel_view::{self, ChannelView},
- chat_panel::ChatPanel,
- face_pile::FacePile,
- panel_settings, CollaborationPanelSettings,
+ channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
+ CollaborationPanelSettings,
};
-use anyhow::Result;
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use channel_modal::ChannelModal;
-use client::{
- proto::{self, PeerId},
- Client, Contact, User, UserStore,
-};
+use client::{Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
-use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
-use drag_and_drop::{DragAndDrop, Draggable};
-use editor::{Cancel, Editor};
+use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use futures::StreamExt;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions,
- elements::{
- Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
- ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
- SafeStylable, Stack, Svg,
- },
- fonts::TextStyle,
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- impl_actions,
- platform::{CursorStyle, MouseButton, PromptLevel},
- serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
- ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+ actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
+ AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+ FocusHandle, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model,
+ MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString,
+ Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
};
-use menu::{Confirm, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SelectNext, SelectPrev};
use project::{Fs, Project};
+use rpc::proto::{self, PeerId};
use serde_derive::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton, Interactive};
+use settings::{Settings, SettingsStore};
+use smallvec::SmallVec;
+use std::{mem, sync::Arc};
+use theme::{ActiveTheme, ThemeSettings};
+use ui::prelude::*;
+use ui::{
+ h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
+ Label, ListHeader, ListItem, Tooltip,
+};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
- dock::{DockPosition, Panel},
- item::ItemHandle,
- FollowNextCollaborator, Workspace,
+ dock::{DockPosition, Panel, PanelEvent},
+ notifications::NotifyResultExt,
+ Workspace,
};
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
- location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
- location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleSelectedIx {
- ix: usize,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct InviteMembers {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ManageMembers {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct OpenChannelNotes {
- pub channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelCall {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelChat {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct CopyChannelLink {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartMoveChannelFor {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct MoveChannel {
- to: ChannelId,
-}
-
actions!(
collab_panel,
[
@@ -132,25 +55,6 @@ actions!(
]
);
-impl_actions!(
- collab_panel,
- [
- RemoveChannel,
- NewChannel,
- InviteMembers,
- ManageMembers,
- RenameChannel,
- ToggleCollapse,
- OpenChannelNotes,
- JoinChannelCall,
- JoinChannelChat,
- CopyChannelLink,
- StartMoveChannelFor,
- MoveChannel,
- ToggleSelectedIx
- ]
-);
-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ChannelMoveClipboard {
channel_id: ChannelId,
@@ -159,90 +63,12 @@ struct ChannelMoveClipboard {
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
pub fn init(cx: &mut AppContext) {
- settings::register::<panel_settings::CollaborationPanelSettings>(cx);
- contact_finder::init(cx);
- channel_modal::init(cx);
- channel_view::init(cx);
-
- cx.add_action(CollabPanel::cancel);
- cx.add_action(CollabPanel::select_next);
- cx.add_action(CollabPanel::select_prev);
- cx.add_action(CollabPanel::confirm);
- cx.add_action(CollabPanel::insert_space);
- cx.add_action(CollabPanel::remove);
- cx.add_action(CollabPanel::remove_selected_channel);
- cx.add_action(CollabPanel::show_inline_context_menu);
- cx.add_action(CollabPanel::new_subchannel);
- cx.add_action(CollabPanel::invite_members);
- cx.add_action(CollabPanel::manage_members);
- cx.add_action(CollabPanel::rename_selected_channel);
- cx.add_action(CollabPanel::rename_channel);
- cx.add_action(CollabPanel::toggle_channel_collapsed_action);
- cx.add_action(CollabPanel::collapse_selected_channel);
- cx.add_action(CollabPanel::expand_selected_channel);
- cx.add_action(CollabPanel::open_channel_notes);
- cx.add_action(CollabPanel::join_channel_chat);
- cx.add_action(CollabPanel::copy_channel_link);
-
- cx.add_action(
- |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
- if panel.selection.take() != Some(action.ix) {
- panel.selection = Some(action.ix)
- }
-
- cx.notify();
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel,
- action: &StartMoveChannelFor,
- _: &mut ViewContext<CollabPanel>| {
- panel.channel_clipboard = Some(ChannelMoveClipboard {
- channel_id: action.channel_id,
- });
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
- if let Some(channel) = panel.selected_channel() {
- panel.channel_clipboard = Some(ChannelMoveClipboard {
- channel_id: channel.id,
- })
- }
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
- let Some(clipboard) = panel.channel_clipboard.take() else {
- return;
- };
- let Some(selected_channel) = panel.selected_channel() else {
- return;
- };
-
- panel
- .channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
- })
- .detach_and_log_err(cx)
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
- if let Some(clipboard) = panel.channel_clipboard.take() {
- panel.channel_store.update(cx, |channel_store, cx| {
- channel_store
- .move_channel(clipboard.channel_id, Some(action.to), cx)
- .detach_and_log_err(cx)
- })
- }
- },
- );
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::<CollabPanel>(cx);
+ });
+ })
+ .detach();
}
#[derive(Debug)]
@@ -258,58 +84,42 @@ pub enum ChannelEditingState {
}
impl ChannelEditingState {
- fn pending_name(&self) -> Option<&str> {
+ fn pending_name(&self) -> Option<String> {
match self {
- ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
- ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+ ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
+ ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
}
}
}
pub struct CollabPanel {
- width: Option<f32>,
+ width: Option<Pixels>,
fs: Arc<dyn Fs>,
- has_focus: bool,
+ focus_handle: FocusHandle,
channel_clipboard: Option<ChannelMoveClipboard>,
pending_serialization: Task<Option<()>>,
- context_menu: ViewHandle<ContextMenu>,
- filter_editor: ViewHandle<Editor>,
- channel_name_editor: ViewHandle<Editor>,
+ context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
+ list_state: ListState,
+ filter_editor: View<Editor>,
+ channel_name_editor: View<Editor>,
channel_editing_state: Option<ChannelEditingState>,
entries: Vec<ListEntry>,
selection: Option<usize>,
- user_store: ModelHandle<UserStore>,
+ channel_store: Model<ChannelStore>,
+ user_store: Model<UserStore>,
client: Arc<Client>,
- channel_store: ModelHandle<ChannelStore>,
- project: ModelHandle<Project>,
+ project: Model<Project>,
match_candidates: Vec<StringMatchCandidate>,
- list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>,
- drag_target_channel: ChannelDragTarget,
- workspace: WeakViewHandle<Workspace>,
- context_menu_on_selected: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum ChannelDragTarget {
- None,
- Root,
- Channel(ChannelId),
+ workspace: WeakView<Workspace>,
}
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
- width: Option<f32>,
- collapsed_channels: Option<Vec<ChannelId>>,
-}
-
-#[derive(Debug)]
-pub enum Event {
- DockPositionChanged,
- Focus,
- Dismissed,
+ width: Option<Pixels>,
+ collapsed_channels: Option<Vec<u64>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@@ -365,28 +175,17 @@ enum ListEntry {
ContactPlaceholder,
}
-impl Entity for CollabPanel {
- type Event = Event;
-}
-
impl CollabPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
- cx.add_view::<Self, _>(|cx| {
- let view_id = cx.view_id();
-
- let filter_editor = cx.add_view(|cx| {
- let mut editor = Editor::single_line(
- Some(Arc::new(|theme| {
- theme.collab_panel.user_query_editor.clone()
- })),
- cx,
- );
- editor.set_placeholder_text("Filter channels, contacts", cx);
+ pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+ cx.new_view(|cx| {
+ let filter_editor = cx.new_view(|cx| {
+ let mut editor = Editor::single_line(cx);
+ editor.set_placeholder_text("Filter...", cx);
editor
});
- cx.subscribe(&filter_editor, |this, _, event, cx| {
- if let editor::Event::BufferEdited = event {
+ cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
+ if let editor::EditorEvent::BufferEdited = event {
let query = this.filter_editor.read(cx).text(cx);
if !query.is_empty() {
this.selection.take();
@@ -398,27 +197,14 @@ impl CollabPanel {
.iter()
.position(|entry| !matches!(entry, ListEntry::Header(_)));
}
- } else if let editor::Event::Blurred = event {
- let query = this.filter_editor.read(cx).text(cx);
- if query.is_empty() {
- this.selection.take();
- this.update_entries(true, cx);
- }
}
})
.detach();
- let channel_name_editor = cx.add_view(|cx| {
- Editor::single_line(
- Some(Arc::new(|theme| {
- theme.collab_panel.user_query_editor.clone()
- })),
- cx,
- )
- });
+ let channel_name_editor = cx.new_view(|cx| Editor::single_line(cx));
- cx.subscribe(&channel_name_editor, |this, _, event, cx| {
- if let editor::Event::Blurred = event {
+ cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
+ if let editor::EditorEvent::Blurred = event {
if let Some(state) = &this.channel_editing_state {
if state.pending_name().is_some() {
return;
@@ -431,151 +217,31 @@ impl CollabPanel {
})
.detach();
+ let view = cx.view().downgrade();
let list_state =
- ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- let theme = theme::current(cx).clone();
- let is_selected = this.selection == Some(ix);
- let current_project_id = this.project.read(cx).remote_id();
-
- match &this.entries[ix] {
- ListEntry::Header(section) => {
- let is_collapsed = this.collapsed_sections.contains(section);
- this.render_header(*section, &theme, is_selected, is_collapsed, cx)
- }
- ListEntry::CallParticipant {
- user,
- peer_id,
- is_pending,
- } => Self::render_call_participant(
- user,
- *peer_id,
- this.user_store.clone(),
- *is_pending,
- is_selected,
- &theme,
- cx,
- ),
- ListEntry::ParticipantProject {
- project_id,
- worktree_root_names,
- host_user_id,
- is_last,
- } => Self::render_participant_project(
- *project_id,
- worktree_root_names,
- *host_user_id,
- Some(*project_id) == current_project_id,
- *is_last,
- is_selected,
- &theme,
- cx,
- ),
- ListEntry::ParticipantScreen { peer_id, is_last } => {
- Self::render_participant_screen(
- *peer_id,
- *is_last,
- is_selected,
- &theme.collab_panel,
- cx,
- )
- }
- ListEntry::Channel {
- channel,
- depth,
- has_children,
- } => {
- let channel_row = this.render_channel(
- &*channel,
- *depth,
- &theme,
- is_selected,
- *has_children,
- ix,
- cx,
- );
-
- if is_selected && this.context_menu_on_selected {
- Stack::new()
- .with_child(channel_row)
- .with_child(
- ChildView::new(&this.context_menu, cx)
- .aligned()
- .bottom()
- .right(),
- )
- .into_any()
- } else {
- return channel_row;
- }
- }
- ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
- *channel_id,
- &theme.collab_panel,
- is_selected,
- ix,
- cx,
- ),
- ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
- *channel_id,
- &theme.collab_panel,
- is_selected,
- ix,
- cx,
- ),
- ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
- channel.clone(),
- this.channel_store.clone(),
- &theme.collab_panel,
- is_selected,
- cx,
- ),
- ListEntry::IncomingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.collab_panel,
- true,
- is_selected,
- cx,
- ),
- ListEntry::OutgoingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.collab_panel,
- false,
- is_selected,
- cx,
- ),
- ListEntry::Contact { contact, calling } => Self::render_contact(
- contact,
- *calling,
- &this.project,
- &theme,
- is_selected,
- cx,
- ),
- ListEntry::ChannelEditor { depth } => {
- this.render_channel_editor(&theme, *depth, cx)
- }
- ListEntry::ContactPlaceholder => {
- this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
- }
+ ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
+ if let Some(view) = view.upgrade() {
+ view.update(cx, |view, cx| view.render_list_entry(ix, cx))
+ } else {
+ div().into_any()
}
});
let mut this = Self {
width: None,
- has_focus: false,
+ focus_handle: cx.focus_handle(),
channel_clipboard: None,
fs: workspace.app_state().fs.clone(),
pending_serialization: Task::ready(None),
- context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+ context_menu: None,
+ list_state,
channel_name_editor,
filter_editor,
entries: Vec::default(),
channel_editing_state: None,
selection: None,
- user_store: workspace.user_store().clone(),
channel_store: ChannelStore::global(cx),
+ user_store: workspace.user_store().clone(),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -583,26 +249,22 @@ impl CollabPanel {
collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
- context_menu_on_selected: true,
- drag_target_channel: ChannelDragTarget::None,
- list_state,
};
this.update_entries(false, cx);
// Update the dock position when the setting changes.
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.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(PanelEvent::ChangePosition);
+ }
+ cx.notify();
+ },
+ ));
let active_call = ActiveCall::global(cx);
this.subscriptions
@@ -642,49 +304,41 @@ impl CollabPanel {
})
}
- 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(COLLABORATION_PANEL_KEY) })
- .await
- .log_err()
- .flatten()
- {
- match serde_json::from_str::<SerializedCollabPanel>(&panel) {
- Ok(panel) => Some(panel),
- Err(err) => {
- log::error!("Failed to deserialize collaboration panel: {}", err);
- None
- }
- }
- } else {
- None
- };
-
- workspace.update(&mut cx, |workspace, cx| {
- let panel = CollabPanel::new(workspace, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width;
- panel.collapsed_channels = serialized_panel
- .collapsed_channels
- .unwrap_or_else(|| Vec::new());
- cx.notify();
- });
- }
- panel
- })
+ pub async fn load(
+ workspace: WeakView<Workspace>,
+ mut cx: AsyncWindowContext,
+ ) -> anyhow::Result<View<Self>> {
+ let serialized_panel = cx
+ .background_executor()
+ .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
+ .await
+ .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
+ .log_err()
+ .flatten()
+ .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
+ .transpose()
+ .log_err()
+ .flatten();
+
+ workspace.update(&mut cx, |workspace, cx| {
+ let panel = CollabPanel::new(workspace, cx);
+ if let Some(serialized_panel) = serialized_panel {
+ panel.update(cx, |panel, cx| {
+ panel.width = serialized_panel.width;
+ panel.collapsed_channels = serialized_panel
+ .collapsed_channels
+ .unwrap_or_else(|| Vec::new());
+ cx.notify();
+ });
+ }
+ panel
})
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
let collapsed_channels = self.collapsed_channels.clone();
- self.pending_serialization = cx.background().spawn(
+ self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
@@ -701,11 +355,15 @@ impl CollabPanel {
);
}
+ fn scroll_to_item(&mut self, ix: usize) {
+ self.list_state.scroll_to_reveal_item(ix)
+ }
+
fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.read(cx);
let user_store = self.user_store.read(cx);
let query = self.filter_editor.read(cx).text(cx);
- let executor = cx.background().clone();
+ let executor = cx.background_executor().clone();
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
let old_entries = mem::take(&mut self.entries);
@@ -851,7 +509,7 @@ impl CollabPanel {
.extend(channel_store.ordered_channels().enumerate().map(
|(ix, (_, channel))| StringMatchCandidate {
id: ix,
- string: channel.name.clone(),
+ string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
},
));
@@ -929,7 +587,7 @@ impl CollabPanel {
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
StringMatchCandidate {
id: ix,
- string: channel.name.clone(),
+ string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
}
}));
@@ -1097,7 +755,6 @@ impl CollabPanel {
}
let old_scroll_top = self.list_state.logical_scroll_top();
-
self.list_state.reset(self.entries.len());
if scroll_to_top {
@@ -1121,7 +778,7 @@ impl CollabPanel {
.position(|entry| entry == entry_after_old_top)?;
Some(ListOffset {
item_ix,
- offset_in_item: 0.,
+ offset_in_item: Pixels::ZERO,
})
})
.or_else(|| {
@@ -1133,7 +790,7 @@ impl CollabPanel {
.position(|entry| entry == entry_before_old_top)?;
Some(ListOffset {
item_ix,
- offset_in_item: 0.,
+ offset_in_item: Pixels::ZERO,
})
});
@@ -1146,288 +803,107 @@ impl CollabPanel {
}
fn render_call_participant(
- user: &User,
+ &self,
+ user: &Arc<User>,
peer_id: Option<PeerId>,
- user_store: ModelHandle<UserStore>,
is_pending: bool,
is_selected: bool,
- theme: &theme::Theme,
cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum CallParticipant {}
- enum CallParticipantTooltip {}
- enum LeaveCallButton {}
- enum LeaveCallTooltip {}
-
- let collab_theme = &theme.collab_panel;
-
+ ) -> ListItem {
let is_current_user =
- user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
- let content = MouseEventHandler::new::<CallParticipant, _>(
- user.id as usize,
- cx,
- |mouse_state, cx| {
- let style = if is_current_user {
- *collab_theme
- .contact_row
- .in_state(is_selected)
- .style_for(&mut Default::default())
- } else {
- *collab_theme
- .contact_row
- .in_state(is_selected)
- .style_for(mouse_state)
- };
-
- Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(collab_theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- collab_theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(collab_theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_children(if is_pending {
- Some(
- Label::new("Calling", collab_theme.calling_indicator.text.clone())
- .contained()
- .with_style(collab_theme.calling_indicator.container)
- .aligned()
- .into_any(),
- )
- } else if is_current_user {
- Some(
- MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
- render_icon_button(
- theme
- .collab_panel
- .leave_call_button
- .style_for(is_selected, state),
- "icons/exit.svg",
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, _, cx| {
- Self::leave_call(cx);
- })
- .with_tooltip::<LeaveCallTooltip>(
- 0,
- "Leave call",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any(),
- )
- } else {
- None
- })
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(style)
- },
- );
-
- if is_current_user || is_pending || peer_id.is_none() {
- return content.into_any();
- }
-
+ self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
let tooltip = format!("Follow {}", user.github_login);
- content
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace
- .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
- .map(|task| task.detach_and_log_err(cx));
- }
+ ListItem::new(SharedString::from(user.github_login.clone()))
+ .start_slot(Avatar::new(user.avatar_uri.clone()))
+ .child(Label::new(user.github_login.clone()))
+ .selected(is_selected)
+ .end_slot(if is_pending {
+ Label::new("Calling").color(Color::Muted).into_any_element()
+ } else if is_current_user {
+ IconButton::new("leave-call", Icon::Exit)
+ .style(ButtonStyle::Subtle)
+ .on_click(move |_, cx| Self::leave_call(cx))
+ .tooltip(|cx| Tooltip::text("Leave Call", cx))
+ .into_any_element()
+ } else {
+ div().into_any_element()
+ })
+ .when_some(peer_id, |this, peer_id| {
+ this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+ .on_click(cx.listener(move |this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+ .ok();
+ }))
})
- .with_cursor_style(CursorStyle::PointingHand)
- .with_tooltip::<CallParticipantTooltip>(
- user.id as usize,
- tooltip,
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
}
fn render_participant_project(
+ &self,
project_id: u64,
worktree_root_names: &[String],
host_user_id: u64,
- is_current: bool,
is_last: bool,
is_selected: bool,
- theme: &theme::Theme,
cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum JoinProject {}
- enum JoinProjectTooltip {}
-
- let collab_theme = &theme.collab_panel;
- let host_avatar_width = collab_theme
- .contact_avatar
- .width
- .or(collab_theme.contact_avatar.height)
- .unwrap_or(0.);
- let tree_branch = collab_theme.tree_branch;
- let project_name = if worktree_root_names.is_empty() {
+ ) -> impl IntoElement {
+ let project_name: SharedString = if worktree_root_names.is_empty() {
"untitled".to_string()
} else {
worktree_root_names.join(", ")
- };
-
- let content =
- MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
- let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- let row = if is_current {
- collab_theme
- .project_row
- .in_state(true)
- .style_for(&mut Default::default())
- } else {
- collab_theme
- .project_row
- .in_state(is_selected)
- .style_for(mouse_state)
- };
-
- Flex::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- is_last,
- vec2f(host_avatar_width, collab_theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/file_icons/folder.svg")
- .with_color(collab_theme.channel_hash.color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new(project_name.clone(), row.name.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(row.name.container)
- .flex(1., false),
- )
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(row.container)
- });
-
- if is_current {
- return content.into_any();
}
-
- content
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(project_id, host_user_id, app_state, cx)
- .detach_and_log_err(cx);
- }
- })
- .with_tooltip::<JoinProjectTooltip>(
- project_id as usize,
- format!("Open {}", project_name),
- None,
- theme.tooltip.clone(),
- cx,
+ .into();
+
+ ListItem::new(project_id as usize)
+ .selected(is_selected)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ let app_state = workspace.app_state().clone();
+ workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+ .detach_and_log_err(cx);
+ })
+ .ok();
+ }))
+ .start_slot(
+ h_stack()
+ .gap_1()
+ .child(render_tree_branch(is_last, cx))
+ .child(IconButton::new(0, Icon::Folder)),
)
- .into_any()
+ .child(Label::new(project_name.clone()))
+ .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
}
fn render_participant_screen(
+ &self,
peer_id: Option<PeerId>,
is_last: bool,
is_selected: bool,
- theme: &theme::CollabPanel,
cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum OpenSharedScreen {}
-
- let host_avatar_width = theme
- .contact_avatar
- .width
- .or(theme.contact_avatar.height)
- .unwrap_or(0.);
- let tree_branch = theme.tree_branch;
-
- let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
- peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
- cx,
- |mouse_state, cx| {
- let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- let row = theme
- .project_row
- .in_state(is_selected)
- .style_for(mouse_state);
-
- Flex::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- is_last,
- vec2f(host_avatar_width, theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/desktop.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new("Screen", row.name.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(row.name.container)
- .flex(1., false),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(row.container)
- },
- );
- if peer_id.is_none() {
- return handler.into_any();
- }
- handler
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(peer_id.unwrap(), cx)
- });
- }
+ ) -> impl IntoElement {
+ let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
+
+ ListItem::new(("screen", id))
+ .selected(is_selected)
+ .start_slot(
+ h_stack()
+ .gap_1()
+ .child(render_tree_branch(is_last, cx))
+ .child(IconButton::new(0, Icon::Screen)),
+ )
+ .child(Label::new("Screen"))
+ .when_some(peer_id, |this, _| {
+ this.on_click(cx.listener(move |this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_shared_screen(peer_id.unwrap(), cx)
+ })
+ .ok();
+ }))
+ .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
})
- .into_any()
}
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -3,19 +3,17 @@ 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,
+ actions, div, overlay, AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusableView,
+ Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext,
+ WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use std::sync::Arc;
+use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt;
-use workspace::Modal;
+use workspace::ModalView;
actions!(
channel_modal,
@@ -27,34 +25,27 @@ actions!(
]
);
-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>,
+ picker: View<Picker<ChannelModalDelegate>>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
- has_focus: bool,
}
impl ChannelModal {
pub fn new(
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<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| {
+ let channel_modal = cx.view().downgrade();
+ let picker = cx.new_view(|cx| {
Picker::new(
ChannelModalDelegate {
+ channel_modal,
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
@@ -62,33 +53,24 @@ impl ChannelModal {
channel_store: channel_store.clone(),
channel_id,
match_candidates: Vec::new(),
+ context_menu: None,
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())
+ .modal(false)
});
- 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 {
+ let mode = match self.picker.read(cx).delegate.mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
@@ -103,20 +85,20 @@ impl ChannelModal {
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);
+ .update(cx, |picker, _| picker.delegate.members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
@@ -129,204 +111,118 @@ impl ChannelModal {
.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 set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
+ self.channel_store.update(cx, |channel_store, cx| {
+ channel_store
+ .set_channel_visibility(
+ self.channel_id,
+ match selection {
+ Selection::Unselected => ChannelVisibility::Members,
+ Selection::Selected => ChannelVisibility::Public,
+ Selection::Indeterminate => return,
+ },
+ cx,
+ )
+ .detach_and_log_err(cx)
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(PickerEvent::Dismiss);
+ cx.emit(DismissEvent);
}
}
-impl Entity for ChannelModal {
- type Event = PickerEvent;
-}
+impl EventEmitter<DismissEvent> for ChannelModal {}
+impl ModalView for ChannelModal {}
-impl View for ChannelModal {
- fn ui_name() -> &'static str {
- "ChannelModal"
+impl FocusableView for ChannelModal {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
}
+}
- 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();
+impl Render for ChannelModal {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let channel_store = self.channel_store.read(cx);
+ let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
+ return div();
};
-
- 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,
+ let channel_name = channel.name.clone();
+ let channel_id = channel.id;
+ let visibility = channel.visibility;
+ let mode = self.picker.read(cx).delegate.mode;
+
+ v_stack()
+ .key_context("ChannelModal")
+ .on_action(cx.listener(Self::toggle_mode))
+ .on_action(cx.listener(Self::dismiss))
+ .elevation_3(cx)
+ .w(rems(34.))
+ .child(
+ v_stack()
+ .px_2()
+ .py_1()
+ .rounded_t(px(8.))
+ .bg(cx.theme().colors().element_background)
+ .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
+ .child(Label::new(channel_name))
+ .child(
+ h_stack()
+ .w_full()
+ .justify_between()
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "is-public",
+ if visibility == ChannelVisibility::Public {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
+ )
+ .on_click(cx.listener(Self::set_channel_visiblity)),
)
- })
- .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()),
+ .child(Label::new("Public")),
+ )
+ .children(if visibility == ChannelVisibility::Public {
+ Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
+ 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);
+ }
+ },
+ )))
+ } else {
+ None
+ }),
)
- .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),
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_row()
+ .child(
+ Button::new("manage-members", "Manage Members")
+ .selected(mode == Mode::ManageMembers)
+ .on_click(cx.listener(|this, _, cx| {
+ this.set_mode(Mode::ManageMembers, cx);
+ })),
+ )
+ .child(
+ Button::new("invite-members", "Invite Members")
+ .selected(mode == Mode::InviteMembers)
+ .on_click(cx.listener(|this, _, cx| {
+ this.set_mode(Mode::InviteMembers, cx);
+ })),
+ ),
+ ),
)
- .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,
- }
+ .child(self.picker.clone())
}
}
@@ -337,19 +233,22 @@ pub enum Mode {
}
pub struct ChannelModalDelegate {
+ channel_modal: WeakView<ChannelModal>,
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
- context_menu: ViewHandle<ContextMenu>,
+ context_menu: Option<(View<ContextMenu>, Subscription)>,
}
impl PickerDelegate for ChannelModalDelegate {
+ type ListItem = ListItem;
+
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
@@ -382,19 +281,19 @@ impl PickerDelegate for ChannelModalDelegate {
}
}));
- let matches = cx.background().block(match_strings(
+ let matches = cx.background_executor().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
- cx.background().clone(),
+ cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
@@ -412,8 +311,7 @@ impl PickerDelegate for ChannelModalDelegate {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
- delegate.matching_users = users;
+ picker.delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
@@ -429,11 +327,11 @@ impl PickerDelegate for ChannelModalDelegate {
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)
+ self.show_context_menu(selected_user, 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);
+ self.remove_member(selected_user.id, cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
@@ -445,138 +343,70 @@ impl PickerDelegate for ChannelModalDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ if self.context_menu.is_none() {
+ self.channel_modal
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
+ }
}
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();
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let (user, role) = self.user_at_index(ix)?;
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
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .selected(selected)
+ .start_slot(Avatar::new(user.avatar_uri.clone()))
+ .child(Label::new(user.github_login.clone()))
+ .end_slot(h_stack().gap_2().map(|slot| {
+ match self.mode {
+ Mode::ManageMembers => slot
+ .children(
+ if request_status == Some(proto::channel_member::Kind::Invitee) {
+ Some(Label::new("Invited"))
+ } else {
+ None
+ },
+ )
+ .children(match role {
+ Some(ChannelRole::Admin) => Some(Label::new("Admin")),
+ Some(ChannelRole::Guest) => Some(Label::new("Guest")),
+ _ => None,
+ })
+ .child(IconButton::new("ellipsis", Icon::Ellipsis))
+ .children(
+ if let (Some((menu, _)), true) = (&self.context_menu, selected) {
+ Some(
+ overlay()
+ .anchor(gpui::AnchorCorner::TopLeft)
+ .child(menu.clone()),
+ )
+ } else {
+ None
+ },
+ ),
+ Mode::InviteMembers => match request_status {
+ Some(proto::channel_member::Kind::Invitee) => {
+ slot.children(Some(Label::new("Invited")))
+ }
+ Some(proto::channel_member::Kind::Member) => {
+ slot.children(Some(Label::new("Member")))
+ }
+ _ => slot,
+ },
+ }
+ })),
+ )
}
}
@@ -610,21 +440,20 @@ impl ChannelModalDelegate {
}
}
- 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
- };
+ fn set_user_role(
+ &mut self,
+ user_id: UserId,
+ new_role: ChannelRole,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<()> {
let update = self.channel_store.update(cx, |store, cx| {
- store.set_member_role(self.channel_id, user.id, new_role, 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) {
+ let this = &mut picker.delegate;
+ if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
member.role = new_role;
}
cx.focus_self();
@@ -635,16 +464,14 @@ impl ChannelModalDelegate {
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;
+ fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
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();
+ let this = &mut picker.delegate;
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| {
@@ -661,7 +488,7 @@ impl ChannelModalDelegate {
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
- cx.focus_self();
+ picker.focus(cx);
cx.notify();
})
})
@@ -683,7 +510,7 @@ impl ChannelModalDelegate {
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
- let members = &mut this.delegate_mut().members;
+ let members = &mut this.delegate.members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
@@ -694,24 +521,55 @@ impl ChannelModalDelegate {
.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,
- )
- })
+ fn show_context_menu(
+ &mut self,
+ user: Arc<User>,
+ role: ChannelRole,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) {
+ let user_id = user.id;
+ let picker = cx.view().clone();
+ let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
+ menu = menu.entry("Remove Member", None, {
+ let picker = picker.clone();
+ move |cx| {
+ picker.update(cx, |picker, cx| {
+ picker.delegate.remove_member(user_id, cx);
+ })
+ }
+ });
+
+ let picker = picker.clone();
+ match role {
+ ChannelRole::Admin => {
+ menu = menu.entry("Revoke Admin", None, move |cx| {
+ picker.update(cx, |picker, cx| {
+ picker
+ .delegate
+ .set_user_role(user_id, ChannelRole::Member, cx);
+ })
+ });
+ }
+ ChannelRole::Member => {
+ menu = menu.entry("Make Admin", None, move |cx| {
+ picker.update(cx, |picker, cx| {
+ picker
+ .delegate
+ .set_user_role(user_id, ChannelRole::Admin, cx);
+ })
+ });
+ }
+ _ => {}
+ };
+
+ menu
+ });
+ cx.focus_view(&context_menu);
+ let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
+ picker.delegate.context_menu = None;
+ picker.focus(cx);
+ cx.notify();
+ });
+ self.context_menu = Some((context_menu, subscription));
}
}
@@ -1,42 +1,30 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
- elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+ AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _,
+ Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
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)
-}
+use theme::ActiveTheme as _;
+use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
+use util::{ResultExt as _, TryFutureExt};
+use workspace::ModalView;
pub struct ContactFinder {
- picker: ViewHandle<Picker<ContactFinderDelegate>>,
- has_focus: bool,
+ picker: View<Picker<ContactFinderDelegate>>,
}
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();
+ pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+ let delegate = ContactFinderDelegate {
+ parent: cx.view().downgrade(),
+ user_store,
+ potential_contacts: Arc::from([]),
+ selected_index: 0,
+ };
+ let picker = cx.new_view(|cx| Picker::new(delegate, cx).modal(false));
- Self {
- picker,
- has_focus: false,
- }
+ Self { picker }
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
@@ -44,101 +32,45 @@ impl ContactFinder {
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),
+impl Render for ContactFinder {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_stack()
+ .elevation_3(cx)
+ .child(
+ v_stack()
+ .px_2()
+ .py_1()
+ .bg(cx.theme().colors().element_background)
+ // HACK: Prevent the background color from overflowing the parent container.
+ .rounded_t(px(8.))
+ .child(Label::new("Contacts"))
+ .child(h_stack().child(Label::new("Invite new contacts"))),
)
- .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,
- }
+ .child(self.picker.clone())
+ .w(rems(34.))
}
}
pub struct ContactFinderDelegate {
+ parent: WeakView<ContactFinder>,
potential_contacts: Arc<[Arc<User>]>,
- user_store: ModelHandle<UserStore>,
+ user_store: Model<UserStore>,
selected_index: usize,
}
-impl PickerDelegate for ContactFinderDelegate {
- fn placeholder_text(&self) -> Arc<str> {
- "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> for ContactFinder {}
+impl ModalView for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
}
+}
+
+impl PickerDelegate for ContactFinderDelegate {
+ type ListItem = ListItem;
fn match_count(&self) -> usize {
self.potential_contacts.len()
@@ -152,6 +84,10 @@ impl PickerDelegate for ContactFinderDelegate {
self.selected_index = ix;
}
+ fn placeholder_text(&self) -> Arc<str> {
+ "Search collaborator by username...".into()
+ }
+
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
@@ -161,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
- picker.delegate_mut().potential_contacts = potential_contacts.into();
+ picker.delegate.potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
@@ -191,19 +127,17 @@ impl PickerDelegate for ContactFinderDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ self.parent
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
}
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;
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
@@ -214,48 +148,16 @@ impl PickerDelegate for ContactFinderDelegate {
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()
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .selected(selected)
+ .start_slot(Avatar::new(user.avatar_uri.clone()))
+ .child(Label::new(user.github_login.clone()))
+ .end_slot::<IconElement>(
+ icon_path.map(|icon_path| IconElement::from_path(icon_path)),
+ ),
+ )
}
}
@@ -1,30 +1,24 @@
-use crate::{
- face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
- ToggleDeafen, ToggleMute, ToggleScreenSharing,
-};
+use crate::face_pile::FacePile;
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
-use clock::ReplicaId;
-use context_menu::{ContextMenu, ContextMenuItem};
+use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
use gpui::{
- actions,
- color::Color,
- elements::*,
- geometry::{rect::RectF, vector::vec2f, PathBuilder},
- json::{self, ToJson},
- platform::{CursorStyle, MouseButton},
- AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
- WeakViewHandle,
+ actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
+ InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
+ StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+ WindowBounds,
};
-use picker::PickerEvent;
use project::{Project, RepositoryEntry};
-use recent_projects::{build_recent_projects, RecentProjects};
-use std::{ops::Range, sync::Arc};
-use theme::{AvatarStyle, Theme};
+use recent_projects::RecentProjects;
+use std::sync::Arc;
+use theme::{ActiveTheme, PlayerColors};
+use ui::{
+ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
+ IconButton, IconElement, Tooltip,
+};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
+use workspace::{notifications::NotifyResultExt, Workspace};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -32,131 +26,269 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
actions!(
collab,
[
- ToggleUserMenu,
- ToggleProjectMenu,
- SwitchBranch,
ShareProject,
UnshareProject,
+ ToggleUserMenu,
+ ToggleProjectMenu,
+ SwitchBranch
]
);
pub fn init(cx: &mut AppContext) {
- cx.add_action(CollabTitlebarItem::share_project);
- cx.add_action(CollabTitlebarItem::unshare_project);
- cx.add_action(CollabTitlebarItem::toggle_user_menu);
- cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
- cx.add_action(CollabTitlebarItem::toggle_project_menu);
+ cx.observe_new_views(|workspace: &mut Workspace, cx| {
+ let titlebar_item = cx.new_view(|cx| CollabTitlebarItem::new(workspace, cx));
+ workspace.set_titlebar_item(titlebar_item.into(), cx)
+ })
+ .detach();
+ // cx.add_action(CollabTitlebarItem::share_project);
+ // cx.add_action(CollabTitlebarItem::unshare_project);
+ // cx.add_action(CollabTitlebarItem::toggle_user_menu);
+ // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+ // cx.add_action(CollabTitlebarItem::toggle_project_menu);
}
pub struct CollabTitlebarItem {
- project: ModelHandle<Project>,
- user_store: ModelHandle<UserStore>,
+ project: Model<Project>,
+ user_store: Model<UserStore>,
client: Arc<Client>,
- workspace: WeakViewHandle<Workspace>,
- branch_popover: Option<ViewHandle<BranchList>>,
- project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
- user_menu: ViewHandle<ContextMenu>,
+ workspace: WeakView<Workspace>,
_subscriptions: Vec<Subscription>,
}
-impl Entity for CollabTitlebarItem {
- type Event = ();
-}
-
-impl View for CollabTitlebarItem {
- fn ui_name() -> &'static str {
- "CollabTitlebarItem"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
- workspace
- } else {
- return Empty::new().into_any();
- };
-
- let theme = theme::current(cx).clone();
- let mut left_container = Flex::row();
- let mut right_container = Flex::row().align_children_center();
-
- left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
-
- let user = self.user_store.read(cx).current_user();
- let peer_id = self.client.peer_id();
- if let Some(((user, peer_id), room)) = user
- .as_ref()
- .zip(peer_id)
- .zip(ActiveCall::global(cx).read(cx).room().cloned())
- {
- if room.read(cx).can_publish() {
- right_container
- .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
- }
- right_container.add_child(self.render_leave_call(&theme, cx));
- let muted = room.read(cx).is_muted(cx);
- let speaking = room.read(cx).is_speaking();
- left_container.add_child(
- self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
- );
- left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
- if room.read(cx).can_publish() {
- right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
- }
- right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
- if room.read(cx).can_publish() {
- right_container
- .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
- }
- }
-
- let status = workspace.read(cx).client().status();
- let status = &*status.borrow();
- if matches!(status, client::Status::Connected { .. }) {
- let avatar = user.as_ref().and_then(|user| user.avatar.clone());
- right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
- } else {
- right_container.add_children(self.render_connection_status(status, cx));
- right_container.add_child(self.render_sign_in_button(&theme, cx));
- right_container.add_child(self.render_user_menu_button(&theme, None, cx));
- }
-
- Stack::new()
- .with_child(left_container)
- .with_child(
- Flex::row()
- .with_child(
- right_container.contained().with_background_color(
- theme
- .titlebar
- .container
- .background_color
- .unwrap_or_else(|| Color::transparent_black()),
- ),
- )
- .aligned()
- .right(),
+impl Render for CollabTitlebarItem {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let room = ActiveCall::global(cx).read(cx).room().cloned();
+ let current_user = self.user_store.read(cx).current_user();
+ let client = self.client.clone();
+ let project_id = self.project.read(cx).remote_id();
+
+ h_stack()
+ .id("titlebar")
+ .justify_between()
+ .w_full()
+ .h(rems(1.75))
+ // Set a non-scaling min-height here to ensure the titlebar is
+ // always at least the height of the traffic lights.
+ .min_h(px(32.))
+ .map(|this| {
+ if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
+ this.pl_2()
+ } else {
+ // Use pixels here instead of a rem-based size because the macOS traffic
+ // lights are a static size, and don't scale with the rest of the UI.
+ this.pl(px(80.))
+ }
+ })
+ .bg(cx.theme().colors().title_bar_background)
+ .on_click(|event, cx| {
+ if event.up.click_count == 2 {
+ cx.zoom_window();
+ }
+ })
+ // left side
+ .child(
+ h_stack()
+ .gap_1()
+ .children(self.render_project_host(cx))
+ .child(self.render_project_name(cx))
+ .children(self.render_project_branch(cx))
+ .when_some(
+ current_user.clone().zip(client.peer_id()).zip(room.clone()),
+ |this, ((current_user, peer_id), room)| {
+ let player_colors = cx.theme().players();
+ let room = room.read(cx);
+ let mut remote_participants =
+ room.remote_participants().values().collect::<Vec<_>>();
+ remote_participants.sort_by_key(|p| p.participant_index.0);
+
+ this.children(self.render_collaborator(
+ ¤t_user,
+ peer_id,
+ true,
+ room.is_speaking(),
+ room.is_muted(cx),
+ &room,
+ project_id,
+ ¤t_user,
+ ))
+ .children(
+ remote_participants.iter().filter_map(|collaborator| {
+ let is_present = project_id.map_or(false, |project_id| {
+ collaborator.location
+ == ParticipantLocation::SharedProject { project_id }
+ });
+
+ let face_pile = self.render_collaborator(
+ &collaborator.user,
+ collaborator.peer_id,
+ is_present,
+ collaborator.speaking,
+ collaborator.muted,
+ &room,
+ project_id,
+ ¤t_user,
+ )?;
+
+ Some(
+ v_stack()
+ .id(("collaborator", collaborator.user.id))
+ .child(face_pile)
+ .child(render_color_ribbon(
+ collaborator.participant_index,
+ player_colors,
+ ))
+ .cursor_pointer()
+ .on_click({
+ let peer_id = collaborator.peer_id;
+ cx.listener(move |this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.follow(peer_id, cx);
+ })
+ .ok();
+ })
+ })
+ .tooltip({
+ let login = collaborator.user.github_login.clone();
+ move |cx| {
+ Tooltip::text(format!("Follow {login}"), cx)
+ }
+ }),
+ )
+ }),
+ )
+ },
+ ),
+ )
+ // right side
+ .child(
+ h_stack()
+ .gap_1()
+ .pr_1()
+ .when_some(room, |this, room| {
+ let room = room.read(cx);
+ let project = self.project.read(cx);
+ let is_local = project.is_local();
+ let is_shared = is_local && project.is_shared();
+ let is_muted = room.is_muted(cx);
+ let is_deafened = room.is_deafened().unwrap_or(false);
+ let is_screen_sharing = room.is_screen_sharing();
+
+ this.when(is_local, |this| {
+ this.child(
+ Button::new(
+ "toggle_sharing",
+ if is_shared { "Unshare" } else { "Share" },
+ )
+ .style(ButtonStyle::Subtle)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(
+ move |this, _, cx| {
+ if is_shared {
+ this.unshare_project(&Default::default(), cx);
+ } else {
+ this.share_project(&Default::default(), cx);
+ }
+ },
+ )),
+ )
+ })
+ .child(
+ IconButton::new("leave-call", ui::Icon::Exit)
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .on_click(move |_, cx| {
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.hang_up(cx))
+ .detach_and_log_err(cx);
+ }),
+ )
+ .child(
+ IconButton::new(
+ "mute-microphone",
+ if is_muted {
+ ui::Icon::MicMute
+ } else {
+ ui::Icon::Mic
+ },
+ )
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .selected(is_muted)
+ .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
+ )
+ .child(
+ IconButton::new(
+ "mute-sound",
+ if is_deafened {
+ ui::Icon::AudioOff
+ } else {
+ ui::Icon::AudioOn
+ },
+ )
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .selected(is_deafened)
+ .tooltip(move |cx| {
+ Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
+ })
+ .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
+ )
+ .child(
+ IconButton::new("screen-share", ui::Icon::Screen)
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .selected(is_screen_sharing)
+ .on_click(move |_, cx| {
+ crate::toggle_screen_sharing(&Default::default(), cx)
+ }),
+ )
+ })
+ .map(|el| {
+ let status = self.client.status();
+ let status = &*status.borrow();
+ if matches!(status, client::Status::Connected { .. }) {
+ el.child(self.render_user_menu_button(cx))
+ } else {
+ el.children(self.render_connection_status(status, cx))
+ .child(self.render_sign_in_button(cx))
+ .child(self.render_user_menu_button(cx))
+ }
+ }),
)
- .into_any()
}
}
+fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
+ let color = colors.color_for_participant(participant_index.0).cursor;
+ canvas(move |bounds, cx| {
+ let mut path = Path::new(bounds.lower_left());
+ let height = bounds.size.height;
+ path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
+ path.line_to(bounds.upper_right() - point(height, px(0.)));
+ path.curve_to(bounds.lower_right(), bounds.upper_right());
+ path.line_to(bounds.lower_left());
+ cx.paint_path(path, color);
+ })
+ .h_1()
+ .w_full()
+}
+
impl CollabTitlebarItem {
- pub fn new(
- workspace: &Workspace,
- workspace_handle: &ViewHandle<Workspace>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
+ pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone();
let client = workspace.app_state().client.clone();
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
- subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
+ subscriptions.push(
+ cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
+ cx.notify()
+ }),
+ );
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
- subscriptions.push(cx.observe_window_activation(|this, active, cx| {
- this.window_activation_changed(active, cx)
- }));
+ subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
Self {
@@ -164,184 +296,132 @@ impl CollabTitlebarItem {
project,
user_store,
client,
- user_menu: cx.add_view(|cx| {
- let view_id = cx.view_id();
- let mut menu = ContextMenu::new(view_id, cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
- branch_popover: None,
- project_popover: None,
_subscriptions: subscriptions,
}
}
- fn collect_title_root_names(
- &self,
- theme: Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let project = self.project.read(cx);
+ // resolve if you are in a room -> render_project_owner
+ // render_project_owner -> resolve if you are in a room -> Option<foo>
- let (name, entry) = {
- let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+ pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+ let host = self.project.read(cx).host()?;
+ let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
+ let participant_index = self
+ .user_store
+ .read(cx)
+ .participant_indices()
+ .get(&host.id)?;
+ Some(
+ div().border().border_color(gpui::red()).child(
+ Button::new("project_owner_trigger", host.github_login.clone())
+ .color(Color::Player(participant_index.0))
+ .style(ButtonStyle::Subtle)
+ .label_size(LabelSize::Small)
+ .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
+ ),
+ )
+ }
+
+ pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
+ let name = {
+ let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
- (worktree.root_name(), worktree.root_git_entry())
+ worktree.root_name()
});
- names_and_branches.next().unwrap_or(("", None))
+ names.next().unwrap_or("")
};
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
- let branch_prepended = entry
- .as_ref()
- .and_then(RepositoryEntry::branch)
- .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
- let project_style = theme.titlebar.project_menu_button.clone();
- let git_style = theme.titlebar.git_menu_button.clone();
- let item_spacing = theme.titlebar.item_spacing;
-
- let mut ret = Flex::row();
+ let workspace = self.workspace.clone();
+ popover_menu("project_name_trigger")
+ .trigger(
+ Button::new("project_name_trigger", name)
+ .style(ButtonStyle::Subtle)
+ .label_size(LabelSize::Small)
+ .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
+ )
+ .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
+ }
- if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
- ret = ret.with_child(project_host)
- }
+ pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+ let entry = {
+ let mut names_and_branches =
+ self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+ let worktree = worktree.read(cx);
+ worktree.root_git_entry()
+ });
- ret = ret.with_child(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
- let style = project_style
- .in_state(self.project_popover.is_some())
- .style_for(mouse_state);
- enum RecentProjectsTooltip {}
- Label::new(name, style.text.clone())
- .contained()
- .with_style(style.container)
- .aligned()
- .left()
- .with_tooltip::<RecentProjectsTooltip>(
- 0,
- "Recent projects",
- Some(Box::new(recent_projects::OpenRecent)),
- theme.tooltip.clone(),
+ names_and_branches.next().flatten()
+ };
+ let workspace = self.workspace.upgrade()?;
+ let branch_name = entry
+ .as_ref()
+ .and_then(RepositoryEntry::branch)
+ .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
+ Some(
+ popover_menu("project_branch_trigger")
+ .trigger(
+ Button::new("project_branch_trigger", branch_name)
+ .color(Color::Muted)
+ .style(ButtonStyle::Subtle)
+ .label_size(LabelSize::Small)
+ .tooltip(move |cx| {
+ Tooltip::with_meta(
+ "Recent Branches",
+ Some(&ToggleVcsMenu),
+ "Local branches only",
cx,
)
- .into_any_named("title-project-name")
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.toggle_project_menu(&Default::default(), cx)
- })
- .on_click(MouseButton::Left, move |_, _, _| {}),
+ }),
)
- .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
- );
- if let Some(git_branch) = branch_prepended {
- ret = ret.with_child(
- Flex::row().with_child(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
- enum BranchPopoverTooltip {}
- let style = git_style
- .in_state(self.branch_popover.is_some())
- .style_for(mouse_state);
- Label::new(git_branch, style.text.clone())
- .contained()
- .with_style(style.container.clone())
- .with_margin_right(item_spacing)
- .aligned()
- .left()
- .with_tooltip::<BranchPopoverTooltip>(
- 0,
- "Recent branches",
- Some(Box::new(ToggleVcsMenu)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any_named("title-project-branch")
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.toggle_vcs_menu(&Default::default(), cx)
- })
- .on_click(MouseButton::Left, move |_, _, _| {}),
- )
- .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
- ),
- )
- }
- ret.into_any()
+ .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
+ )
}
- fn collect_project_host(
+ fn render_collaborator(
&self,
- theme: Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- if ActiveCall::global(cx).read(cx).room().is_none() {
- return None;
- }
- let project = self.project.read(cx);
- let user_store = self.user_store.read(cx);
-
- if project.is_local() {
- return None;
- }
-
- let Some(host) = project.host() else {
- return None;
- };
- let (Some(host_user), Some(participant_index)) = (
- user_store.get_cached_user(host.user_id),
- user_store.participant_indices().get(&host.user_id),
- ) else {
- return None;
- };
-
- enum ProjectHost {}
- enum ProjectHostTooltip {}
+ user: &Arc<User>,
+ peer_id: PeerId,
+ is_present: bool,
+ is_speaking: bool,
+ is_muted: bool,
+ room: &Room,
+ project_id: Option<u64>,
+ current_user: &Arc<User>,
+ ) -> Option<FacePile> {
+ let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
+
+ let pile = FacePile::default()
+ .child(
+ Avatar::new(user.avatar_uri.clone())
+ .grayscale(!is_present)
+ .border_color(if is_speaking {
+ gpui::blue()
+ } else if is_muted {
+ gpui::red()
+ } else {
+ Hsla::default()
+ }),
+ )
+ .children(followers.iter().filter_map(|follower_peer_id| {
+ let follower = room
+ .remote_participants()
+ .values()
+ .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
+ .or_else(|| {
+ (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
+ })?
+ .clone();
- let host_style = theme.titlebar.project_host.clone();
- let selection_style = theme
- .editor
- .selection_style_for_room_participant(participant_index.0);
- let peer_id = host.peer_id.clone();
+ Some(Avatar::new(follower.avatar_uri.clone()))
+ }));
- Some(
- MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
- let mut host_style = host_style.style_for(mouse_state).clone();
- host_style.text.color = selection_style.cursor;
- Label::new(host_user.github_login.clone(), host_style.text)
- .contained()
- .with_style(host_style.container)
- .aligned()
- .left()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- if let Some(task) =
- workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
- }
- }
- })
- .with_tooltip::<ProjectHostTooltip>(
- 0,
- host_user.github_login.clone() + " is sharing this project. Click to follow.",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any_named("project-host"),
- )
+ Some(pile)
}
- fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
- let project = if active {
+ fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
+ let project = if cx.is_window_active() {
Some(self.project.clone())
} else {
None
@@ -371,801 +451,44 @@ impl CollabTitlebarItem {
.log_err();
}
- pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
- self.user_menu.update(cx, |user_menu, cx| {
- let items = if let Some(_) = self.user_store.read(cx).current_user() {
- vec![
- ContextMenuItem::action("Settings", zed_actions::OpenSettings),
- ContextMenuItem::action("Theme", theme_selector::Toggle),
- ContextMenuItem::separator(),
- ContextMenuItem::action(
- "Share Feedback",
- feedback::feedback_editor::GiveFeedback,
- ),
- ContextMenuItem::action("Sign Out", SignOut),
- ]
- } else {
- vec![
- ContextMenuItem::action("Settings", zed_actions::OpenSettings),
- ContextMenuItem::action("Theme", theme_selector::Toggle),
- ContextMenuItem::separator(),
- ContextMenuItem::action(
- "Share Feedback",
- feedback::feedback_editor::GiveFeedback,
- ),
- ]
- };
- user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
- });
- }
-
- fn render_branches_popover_host<'a>(
- &'a self,
- _theme: &'a theme::Titlebar,
- cx: &'a mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- self.branch_popover.as_ref().map(|child| {
- let theme = theme::current(cx).clone();
- let child = ChildView::new(child, cx);
- let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
- child
- .flex(1., true)
- .contained()
- .constrained()
- .with_width(theme.titlebar.menu.width)
- .with_height(theme.titlebar.menu.height)
- })
- .on_click(MouseButton::Left, |_, _, _| {})
- .on_down_out(MouseButton::Left, move |_, this, cx| {
- this.branch_popover.take();
- cx.emit(());
- cx.notify();
- })
- .contained()
- .into_any();
-
- Overlay::new(child)
- .with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopLeft)
- .with_z_index(999)
- .aligned()
- .bottom()
- .left()
- .into_any()
- })
- }
-
- fn render_project_popover_host<'a>(
- &'a self,
- _theme: &'a theme::Titlebar,
- cx: &'a mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- self.project_popover.as_ref().map(|child| {
- let theme = theme::current(cx).clone();
- let child = ChildView::new(child, cx);
- let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
- child
- .flex(1., true)
- .contained()
- .constrained()
- .with_width(theme.titlebar.menu.width)
- .with_height(theme.titlebar.menu.height)
- })
- .on_click(MouseButton::Left, |_, _, _| {})
- .on_down_out(MouseButton::Left, move |_, this, cx| {
- this.project_popover.take();
- cx.emit(());
- cx.notify();
- })
- .into_any();
-
- Overlay::new(child)
- .with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopLeft)
- .with_z_index(999)
- .aligned()
- .bottom()
- .left()
- .into_any()
- })
- }
-
- pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
- if self.branch_popover.take().is_none() {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- let Some(view) =
- cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
- else {
- return;
- };
- cx.subscribe(&view, |this, _, event, cx| {
- match event {
- PickerEvent::Dismiss => {
- this.branch_popover = None;
- }
- }
-
- cx.notify();
- })
- .detach();
- self.project_popover.take();
- cx.focus(&view);
- self.branch_popover = Some(view);
- }
- }
-
- cx.notify();
+ pub fn render_vcs_popover(
+ workspace: View<Workspace>,
+ cx: &mut WindowContext<'_>,
+ ) -> Option<View<BranchList>> {
+ let view = build_branch_list(workspace, cx).log_err()?;
+ let focus_handle = view.focus_handle(cx);
+ cx.focus(&focus_handle);
+ Some(view)
}
- pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
- let workspace = self.workspace.clone();
- if self.project_popover.take().is_none() {
- cx.spawn(|this, mut cx| async move {
- let workspaces = WORKSPACE_DB
- .recent_workspaces_on_disk()
- .await
- .unwrap_or_default()
- .into_iter()
- .map(|(_, location)| location)
- .collect();
-
- let workspace = workspace.clone();
- this.update(&mut cx, move |this, cx| {
- let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
-
- cx.subscribe(&view, |this, _, event, cx| {
- match event {
- PickerEvent::Dismiss => {
- this.project_popover = None;
- }
- }
+ pub fn render_project_popover(
+ workspace: WeakView<Workspace>,
+ cx: &mut WindowContext<'_>,
+ ) -> View<RecentProjects> {
+ let view = RecentProjects::open_popover(workspace, cx);
- cx.notify();
- })
- .detach();
- cx.focus(&view);
- this.branch_popover.take();
- this.project_popover = Some(view);
- cx.notify();
- })
- .log_err();
- })
- .detach();
- }
- cx.notify();
- }
-
- fn render_toggle_screen_sharing_button(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- if room.read(cx).is_screen_sharing() {
- icon = "icons/desktop.svg";
- tooltip = "Stop Sharing Screen"
- } else {
- icon = "icons/desktop.svg";
- tooltip = "Share Screen";
- }
-
- let active = room.read(cx).is_screen_sharing();
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
- let style = titlebar
- .screen_share_button
- .in_state(active)
- .style_for(state);
-
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_screen_sharing(&Default::default(), cx)
- })
- .with_tooltip::<ToggleScreenSharing>(
- 0,
- tooltip,
- Some(Box::new(ToggleScreenSharing)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_toggle_mute(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- let is_muted = room.read(cx).is_muted(cx);
- if is_muted {
- icon = "icons/mic-mute.svg";
- tooltip = "Unmute microphone";
- } else {
- icon = "icons/mic.svg";
- tooltip = "Mute microphone";
- }
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
- let style = titlebar
- .toggle_microphone_button
- .in_state(is_muted)
- .style_for(state);
- let image = Svg::new(icon)
- .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);
- if let Some(color) = style.container.background_color {
- image.with_background_color(color)
- } else {
- image
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_mute(&Default::default(), cx)
- })
- .with_tooltip::<ToggleMute>(
- 0,
- tooltip,
- Some(Box::new(ToggleMute)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_toggle_deafen(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
- if is_deafened {
- icon = "icons/speaker-off.svg";
- tooltip = "Unmute speakers";
- } else {
- icon = "icons/speaker-loud.svg";
- tooltip = "Mute speakers";
- }
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
- let style = titlebar
- .toggle_speakers_button
- .in_state(is_deafened)
- .style_for(state);
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_deafen(&Default::default(), cx)
- })
- .with_tooltip::<ToggleDeafen>(
- 0,
- tooltip,
- Some(Box::new(ToggleDeafen)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let icon = "icons/exit.svg";
- let tooltip = "Leave call";
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
- let style = titlebar.leave_call_button.style_for(state);
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- })
- .with_tooltip::<LeaveCall>(
- 0,
- tooltip,
- Some(Box::new(LeaveCall)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_in_call_share_unshare_button(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- let project = workspace.read(cx).project();
- if project.read(cx).is_remote() {
- return None;
- }
-
- let is_shared = project.read(cx).is_shared();
- let label = if is_shared { "Stop Sharing" } else { "Share" };
- let tooltip = if is_shared {
- "Stop sharing project with call participants"
- } else {
- "Share project with call participants"
- };
-
- let titlebar = &theme.titlebar;
-
- enum ShareUnshare {}
- Some(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
- //TODO: Ensure this button has consistent width for both text variations
- let style = titlebar.share_button.inactive_state().style_for(state);
- Label::new(label, style.text.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if is_shared {
- this.unshare_project(&Default::default(), cx);
- } else {
- this.share_project(&Default::default(), cx);
- }
- })
- .with_tooltip::<ShareUnshare>(
- 0,
- tooltip.to_owned(),
- None,
- theme.tooltip.clone(),
- cx,
- ),
- )
- .aligned()
- .contained()
- .with_margin_left(theme.titlebar.item_spacing)
- .into_any(),
- )
- }
-
- fn render_user_menu_button(
- &self,
- theme: &Theme,
- avatar: Option<Arc<ImageData>>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let tooltip = theme.tooltip.clone();
- let user_menu_button_style = if avatar.is_some() {
- &theme.titlebar.user_menu.user_menu_button_online
- } else {
- &theme.titlebar.user_menu.user_menu_button_offline
- };
-
- let avatar_style = &user_menu_button_style.avatar;
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
- let style = user_menu_button_style
- .user_menu
- .inactive_state()
- .style_for(state);
-
- let mut dropdown = Flex::row().align_children_center();
-
- if let Some(avatar_img) = avatar {
- dropdown = dropdown.with_child(Self::render_face(
- avatar_img,
- *avatar_style,
- Color::transparent_black(),
- None,
- ));
- };
-
- dropdown
- .with_child(
- Svg::new("icons/caret_down.svg")
- .with_color(user_menu_button_style.icon.color)
- .constrained()
- .with_width(user_menu_button_style.icon.width)
- .contained()
- .into_any(),
- )
- .aligned()
- .constrained()
- .with_height(style.width)
- .contained()
- .with_style(style.container)
- .into_any()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.user_menu.update(cx, |menu, _| menu.delay_cancel());
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.toggle_user_menu(&Default::default(), cx)
- })
- .with_tooltip::<ToggleUserMenu>(
- 0,
- "Toggle User Menu".to_owned(),
- Some(Box::new(ToggleUserMenu)),
- tooltip,
- cx,
- )
- .contained(),
- )
- .with_child(
- ChildView::new(&self.user_menu, cx)
- .aligned()
- .bottom()
- .right(),
- )
- .into_any()
- }
-
- fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
- let style = titlebar.sign_in_button.inactive_state().style_for(state);
- Label::new("Sign In", style.text.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let client = this.client.clone();
- cx.app_context()
- .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
- .detach_and_log_err(cx);
- })
- .into_any()
- }
-
- fn render_collaborators(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> Vec<Container<Self>> {
- let mut participants = room
- .read(cx)
- .remote_participants()
- .values()
- .cloned()
- .collect::<Vec<_>>();
- participants.sort_by_cached_key(|p| p.user.github_login.clone());
-
- participants
- .into_iter()
- .filter_map(|participant| {
- let project = workspace.read(cx).project().read(cx);
- let replica_id = project
- .collaborators()
- .get(&participant.peer_id)
- .map(|collaborator| collaborator.replica_id);
- let user = participant.user.clone();
- Some(
- Container::new(self.render_face_pile(
- &user,
- replica_id,
- participant.peer_id,
- Some(participant.location),
- participant.muted,
- participant.speaking,
- workspace,
- theme,
- cx,
- ))
- .with_margin_right(theme.titlebar.face_pile_spacing),
- )
- })
- .collect()
- }
-
- fn render_current_user(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- user: &Arc<User>,
- peer_id: PeerId,
- muted: bool,
- speaking: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let replica_id = workspace.read(cx).project().read(cx).replica_id();
-
- Container::new(self.render_face_pile(
- user,
- Some(replica_id),
- peer_id,
- None,
- muted,
- speaking,
- workspace,
- theme,
- cx,
- ))
- .with_margin_right(theme.titlebar.item_spacing)
- .into_any()
- }
-
- fn render_face_pile(
- &self,
- user: &User,
- _replica_id: Option<ReplicaId>,
- peer_id: PeerId,
- location: Option<ParticipantLocation>,
- muted: bool,
- speaking: bool,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let user_id = user.id;
- let project_id = workspace.read(cx).project().read(cx).remote_id();
- let room = ActiveCall::global(cx).read(cx).room().cloned();
- let self_peer_id = workspace.read(cx).client().peer_id();
- let self_following = workspace.read(cx).is_being_followed(peer_id);
- let self_following_initialized = self_following
- && room.as_ref().map_or(false, |room| match project_id {
- None => true,
- Some(project_id) => room
- .read(cx)
- .followers_for(peer_id, project_id)
- .iter()
- .any(|&follower| Some(follower) == self_peer_id),
- });
-
- let leader_style = theme.titlebar.leader_avatar;
- let follower_style = theme.titlebar.follower_avatar;
-
- let microphone_state = if muted {
- Some(theme.titlebar.muted)
- } else if speaking {
- Some(theme.titlebar.speaking)
- } else {
- None
- };
-
- let mut background_color = theme
- .titlebar
- .container
- .background_color
- .unwrap_or_default();
-
- let participant_index = self
- .user_store
- .read(cx)
- .participant_indices()
- .get(&user_id)
- .copied();
- if let Some(participant_index) = participant_index {
- if self_following_initialized {
- let selection = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .selection;
- background_color = Color::blend(selection, background_color);
- background_color.a = 255;
- }
- }
-
- enum TitlebarParticipant {}
-
- let content = MouseEventHandler::new::<TitlebarParticipant, _>(
- peer_id.as_u64() as usize,
- cx,
- move |_, cx| {
- Stack::new()
- .with_children(user.avatar.as_ref().map(|avatar| {
- let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
- .with_child(Self::render_face(
- avatar.clone(),
- Self::location_style(workspace, location, leader_style, cx),
- background_color,
- microphone_state,
- ))
- .with_children(
- (|| {
- let project_id = project_id?;
- let room = room?.read(cx);
- let followers = room.followers_for(peer_id, project_id);
- Some(followers.into_iter().filter_map(|&follower| {
- if Some(follower) == self_peer_id {
- return None;
- }
- let participant =
- room.remote_participant_for_peer_id(follower)?;
- Some(Self::render_face(
- participant.user.avatar.clone()?,
- follower_style,
- background_color,
- None,
- ))
- }))
- })()
- .into_iter()
- .flatten(),
- )
- .with_children(
- self_following_initialized
- .then(|| self.user_store.read(cx).current_user())
- .and_then(|user| {
- Some(Self::render_face(
- user?.avatar.clone()?,
- follower_style,
- background_color,
- None,
- ))
- }),
- );
-
- let mut container = face_pile
- .contained()
- .with_style(theme.titlebar.leader_selection);
-
- if let Some(participant_index) = participant_index {
- if self_following_initialized {
- let color = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .selection;
- container = container.with_background_color(color);
- }
- }
-
- container
- }))
- .with_children((|| {
- let participant_index = participant_index?;
- let color = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .cursor;
- Some(
- AvatarRibbon::new(color)
- .constrained()
- .with_width(theme.titlebar.avatar_ribbon.width)
- .with_height(theme.titlebar.avatar_ribbon.height)
- .aligned()
- .bottom(),
- )
- })())
- },
- );
-
- if Some(peer_id) == self_peer_id {
- return content.into_any();
- }
-
- content
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let Some(workspace) = this.workspace.upgrade(cx) else {
- return;
- };
- if let Some(task) =
- workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
- }
- })
- .with_tooltip::<TitlebarParticipant>(
- peer_id.as_u64() as usize,
- format!("Follow {}", user.github_login),
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
-
- fn location_style(
- workspace: &ViewHandle<Workspace>,
- location: Option<ParticipantLocation>,
- mut style: AvatarStyle,
- cx: &ViewContext<Self>,
- ) -> AvatarStyle {
- if let Some(location) = location {
- if let ParticipantLocation::SharedProject { project_id } = location {
- if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
- style.image.grayscale = true;
- }
- } else {
- style.image.grayscale = true;
- }
- }
-
- style
- }
-
- fn render_face<V: 'static>(
- avatar: Arc<ImageData>,
- avatar_style: AvatarStyle,
- background_color: Color,
- microphone_state: Option<Color>,
- ) -> AnyElement<V> {
- Image::from_data(avatar)
- .with_style(avatar_style.image)
- .aligned()
- .contained()
- .with_background_color(microphone_state.unwrap_or(background_color))
- .with_corner_radius(avatar_style.outer_corner_radius)
- .constrained()
- .with_width(avatar_style.outer_width)
- .with_height(avatar_style.outer_width)
- .aligned()
- .into_any()
+ let focus_handle = view.focus_handle(cx);
+ cx.focus(&focus_handle);
+ view
}
fn render_connection_status(
&self,
status: &client::Status,
cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- enum ConnectionStatusButton {}
-
- let theme = &theme::current(cx).clone();
+ ) -> Option<AnyElement> {
match status {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
- Svg::new("icons/disconnected.svg")
- .with_color(theme.titlebar.offline_icon.color)
- .constrained()
- .with_width(theme.titlebar.offline_icon.width)
- .aligned()
- .contained()
- .with_style(theme.titlebar.offline_icon.container)
- .into_any(),
+ div()
+ .id("disconnected")
+ .bg(gpui::red()) // todo!() @nate
+ .child(IconElement::new(Icon::Disconnected))
+ .tooltip(|cx| Tooltip::text("Disconnected", cx))
+ .into_any_element(),
),
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
@@ -7,27 +7,22 @@ pub mod notification_panel;
pub mod notifications;
mod panel_settings;
+use std::{rc::Rc, sync::Arc};
+
use call::{report_call_event_for_room, ActiveCall, Room};
+pub use collab_panel::CollabPanel;
+pub use collab_titlebar_item::CollabTitlebarItem;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use gpui::{
- actions,
- elements::{ContainerStyle, Empty, Image},
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- platform::{Screen, WindowBounds, WindowKind, WindowOptions},
- AnyElement, AppContext, Element, ImageData, Task,
+ actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
+ WindowKind, WindowOptions,
};
-use std::{rc::Rc, sync::Arc};
-use theme::AvatarStyle;
-use util::ResultExt;
-use workspace::AppState;
-
-pub use collab_titlebar_item::CollabTitlebarItem;
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
+use settings::Settings;
+use util::ResultExt;
+use workspace::AppState;
actions!(
collab,
@@ -35,19 +30,21 @@ actions!(
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
- settings::register::<CollaborationPanelSettings>(cx);
- settings::register::<ChatPanelSettings>(cx);
- settings::register::<NotificationPanelSettings>(cx);
+ CollaborationPanelSettings::register(cx);
+ ChatPanelSettings::register(cx);
+ NotificationPanelSettings::register(cx);
vcs_menu::init(cx);
collab_titlebar_item::init(cx);
collab_panel::init(cx);
+ channel_view::init(cx);
chat_panel::init(cx);
+ notification_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);
+ // 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) {
@@ -107,58 +104,63 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
}
fn notification_window_options(
- screen: Rc<dyn Screen>,
- window_size: Vector2F,
-) -> WindowOptions<'static> {
- const NOTIFICATION_PADDING: f32 = 16.;
+ screen: Rc<dyn PlatformDisplay>,
+ window_size: Size<Pixels>,
+) -> WindowOptions {
+ let notification_margin_width = GlobalPixels::from(16.);
+ let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
+
+ let screen_bounds = screen.bounds();
+ let size: Size<GlobalPixels> = window_size.into();
- let screen_bounds = screen.content_bounds();
+ // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
+ let bounds = gpui::Bounds::<GlobalPixels> {
+ origin: screen_bounds.upper_right()
+ - point(
+ size.width + notification_margin_width,
+ notification_margin_height,
+ ),
+ size: window_size.into(),
+ };
WindowOptions {
- bounds: WindowBounds::Fixed(RectF::new(
- screen_bounds.upper_right()
- + vec2f(
- -NOTIFICATION_PADDING - window_size.x(),
- NOTIFICATION_PADDING,
- ),
- window_size,
- )),
+ bounds: WindowBounds::Fixed(bounds),
titlebar: None,
center: false,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
- screen: Some(screen),
+ display_id: Some(screen.id()),
}
}
-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 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>()
@@ -1,113 +1,30 @@
-use std::ops::Range;
-
use gpui::{
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- json::ToJson,
- serde_json::{self, json},
- AnyElement, Axis, Element, View, ViewContext,
+ div, AnyElement, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
};
+use smallvec::SmallVec;
-pub(crate) struct FacePile<V: View> {
- overlap: f32,
- faces: Vec<AnyElement<V>>,
+#[derive(Default, IntoElement)]
+pub struct FacePile {
+ pub faces: SmallVec<[AnyElement; 2]>,
}
-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 RenderOnce for FacePile {
+ fn render(self, _: &mut WindowContext) -> impl IntoElement {
+ let player_count = self.faces.len();
+ let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
+ let isnt_last = ix < player_count - 1;
+
+ div()
+ .z_index((player_count - ix) as u8)
+ .when(isnt_last, |div| div.neg_mr_1())
+ .child(player)
+ });
+ div().p_1().flex().items_center().children(player_list)
}
}
-impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
- fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
- self.faces.extend(children);
+impl ParentElement for FacePile {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.faces
}
}
@@ -1,4 +1,4 @@
-use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+use crate::{chat_panel::ChatPanel, NotificationPanelSettings};
use anyhow::Result;
use channel::ChannelStore;
use client::{Client, Notification, User, UserStore};
@@ -6,23 +6,23 @@ 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,
+ actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+ CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
+ InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
+ ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
+ VisualContext, WeakView, WindowContext,
};
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs;
use rpc::proto;
use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
-use theme::{ui, Theme};
use time::{OffsetDateTime, UtcOffset};
+use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
use util::{ResultExt, TryFutureExt};
use workspace::{
- dock::{DockPosition, Panel},
+ dock::{DockPosition, Panel, PanelEvent},
Workspace,
};
@@ -33,25 +33,25 @@ 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>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
+ notification_store: Model<NotificationStore>,
fs: Arc<dyn Fs>,
- width: Option<f32>,
+ width: Option<Pixels>,
active: bool,
- notification_list: ListState<Self>,
+ notification_list: ListState,
pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
- workspace: WeakViewHandle<Workspace>,
+ workspace: WeakView<Workspace>,
current_notification_toast: Option<(u64, Task<()>)>,
local_timezone: UtcOffset,
- has_focus: bool,
+ focus_handle: FocusHandle,
mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
}
#[derive(Serialize, Deserialize)]
struct SerializedNotificationPanel {
- width: Option<f32>,
+ width: Option<Pixels>,
}
#[derive(Debug)]
@@ -71,16 +71,23 @@ pub struct NotificationPresenter {
actions!(notification_panel, [ToggleFocus]);
-pub fn init(_cx: &mut AppContext) {}
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::<NotificationPanel>(cx);
+ });
+ })
+ .detach();
+}
impl NotificationPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+ pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let user_store = workspace.app_state().user_store.clone();
let workspace_handle = workspace.weak_handle();
- cx.add_view(|cx| {
+ cx.new_view(|cx: &mut ViewContext<Self>| {
let mut status = client.status();
cx.spawn(|this, mut cx| async move {
while let Some(_) = status.next().await {
@@ -96,33 +103,39 @@ impl NotificationPanel {
})
.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())
+ let view = cx.view().downgrade();
+ let notification_list =
+ ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
+ view.upgrade()
+ .and_then(|view| {
+ view.update(cx, |this, cx| this.render_notification(ix, cx))
+ })
+ .unwrap_or_else(|| div().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();
+ notification_list.set_scroll_handler(cx.listener(
+ |this, event: &ListScrollEvent, cx| {
+ if event.count.saturating_sub(event.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(),
+ local_timezone: cx.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,
+ focus_handle: cx.focus_handle(),
current_notification_toast: None,
subscriptions: Vec::new(),
active: false,
@@ -134,7 +147,7 @@ impl NotificationPanel {
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| {
+ 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;
@@ -148,12 +161,12 @@ impl NotificationPanel {
}
pub fn load(
- workspace: WeakViewHandle<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
+ workspace: WeakView<Workspace>,
+ cx: AsyncWindowContext,
+ ) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
- .background()
+ .background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
.await
.log_err()
@@ -179,7 +192,7 @@ impl NotificationPanel {
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
- self.pending_serialization = cx.background().spawn(
+ self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
@@ -193,11 +206,7 @@ impl NotificationPanel {
);
}
- fn render_notification(
- &mut self,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
+ fn render_notification(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let entry = self.notification_store.read(cx).notification_at(ix)?;
let notification_id = entry.id;
let now = OffsetDateTime::now_utc();
@@ -210,136 +219,99 @@ impl NotificationPanel {
..
} = 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),
+ div()
+ .id(ix)
+ .flex()
+ .flex_row()
+ .size_full()
+ .px_2()
+ .py_1()
+ .gap_2()
+ .when(can_navigate, |el| {
+ el.cursor(CursorStyle::PointingHand).on_click({
+ let notification = notification.clone();
+ cx.listener(move |this, _, cx| {
+ this.did_click_notification(¬ification, cx)
+ })
+ })
+ })
+ .children(actor.map(|actor| {
+ img(actor.avatar_uri.clone())
+ .flex_none()
+ .w_8()
+ .h_8()
+ .rounded_full()
+ }))
+ .child(
+ v_stack()
+ .gap_1()
+ .size_full()
+ .overflow_hidden()
+ .child(Label::new(text.clone()))
+ .child(
+ h_stack()
+ .child(
+ Label::new(format_timestamp(
+ timestamp,
+ now,
+ self.local_timezone,
+ ))
+ .color(Color::Muted),
+ )
+ .children(if let Some(is_accepted) = response {
+ Some(div().flex().flex_grow().justify_end().child(Label::new(
+ if is_accepted {
+ "You accepted"
+ } else {
+ "You declined"
+ },
+ )))
+ } else if needs_response {
+ Some(
+ h_stack()
+ .flex_grow()
+ .justify_end()
+ .child(Button::new("decline", "Decline").on_click({
+ let notification = notification.clone();
+ let view = cx.view().clone();
+ move |_, cx| {
+ view.update(cx, |this, cx| {
+ this.respond_to_notification(
+ notification.clone(),
+ false,
+ cx,
+ )
+ });
+ }
+ }))
+ .child(Button::new("accept", "Accept").on_click({
+ let notification = notification.clone();
+ let view = cx.view().clone();
+ move |_, cx| {
+ view.update(cx, |this, cx| {
+ this.respond_to_notification(
+ notification.clone(),
+ true,
+ cx,
+ )
+ });
+ }
+ })),
)
- .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(),
+ } else {
+ None
+ }),
+ ),
+ )
+ .into_any(),
)
}
@@ -432,7 +404,7 @@ impl NotificationPanel {
.or_insert_with(|| {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
- cx.background().timer(MARK_AS_READ_DELAY).await;
+ cx.background_executor().timer(MARK_AS_READ_DELAY).await;
client
.request(proto::MarkNotificationRead { notification_id })
.await?;
@@ -452,8 +424,8 @@ impl NotificationPanel {
..
} = notification.clone()
{
- if let Some(workspace) = self.workspace.upgrade(cx) {
- cx.app_context().defer(move |cx| {
+ if let Some(workspace) = self.workspace.upgrade() {
+ cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
panel.update(cx, |panel, cx| {
@@ -468,73 +440,27 @@ impl NotificationPanel {
}
}
- fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+ fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> 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();
+ if let Some(workspace) = self.workspace.upgrade() {
+ return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
+ let panel = panel.read(cx);
+ panel.is_scrolled_to_bottom()
+ && panel
+ .active_chat()
+ .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
+ } else {
+ false
+ };
}
}
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>,
+ _: Model<NotificationStore>,
event: &NotificationEvent,
cx: &mut ViewContext<Self>,
) {
@@ -566,7 +492,7 @@ impl NotificationPanel {
self.current_notification_toast = Some((
notification_id,
cx.spawn(|this, mut cx| async move {
- cx.background().timer(TOAST_DURATION).await;
+ cx.background_executor().timer(TOAST_DURATION).await;
this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
.ok();
}),
@@ -576,8 +502,8 @@ impl NotificationPanel {
.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 {
+ let workspace = cx.view().downgrade();
+ cx.new_view(|_| NotificationToast {
notification_id,
actor,
text,
@@ -613,62 +539,90 @@ impl NotificationPanel {
}
}
-impl Entity for NotificationPanel {
- type Event = Event;
-}
-
-impl View for NotificationPanel {
- fn ui_name() -> &'static str {
- "NotificationPanel"
+impl Render for NotificationPanel {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_stack()
+ .size_full()
+ .child(
+ h_stack()
+ .justify_between()
+ .px_2()
+ .py_1()
+ // Match the height of the tab bar so they line up.
+ .h(rems(ui::Tab::HEIGHT_IN_REMS))
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .child(Label::new("Notifications"))
+ .child(IconElement::new(Icon::Envelope)),
+ )
+ .map(|this| {
+ if self.client.user_id().is_none() {
+ this.child(
+ v_stack()
+ .gap_2()
+ .p_4()
+ .child(
+ Button::new("sign_in_prompt_button", "Sign in")
+ .icon_color(Color::Muted)
+ .icon(Icon::Github)
+ .icon_position(IconPosition::Start)
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .on_click({
+ let client = self.client.clone();
+ move |_, cx| {
+ let client = client.clone();
+ cx.spawn(move |cx| async move {
+ client
+ .authenticate_and_connect(true, &cx)
+ .log_err()
+ .await;
+ })
+ .detach()
+ }
+ }),
+ )
+ .child(
+ div().flex().w_full().items_center().child(
+ Label::new("Sign in to view notifications.")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ ),
+ )
+ } else if self.notification_list.item_count() == 0 {
+ this.child(
+ v_stack().p_4().child(
+ div().flex().w_full().items_center().child(
+ Label::new("You have no notifications.")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ ),
+ )
+ } else {
+ this.child(list(self.notification_list.clone()).size_full())
+ }
+ })
}
+}
- 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()
+impl FocusableView for NotificationPanel {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
}
+}
- fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = true;
- }
+impl EventEmitter<Event> for NotificationPanel {}
+impl EventEmitter<PanelEvent> for NotificationPanel {}
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
+impl Panel for NotificationPanel {
+ fn persistent_name() -> &'static str {
+ "NotificationPanel"
}
-}
-impl Panel for NotificationPanel {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- settings::get::<NotificationPanelSettings>(cx).dock
+ NotificationPanelSettings::get_global(cx).dock
}
fn position_is_valid(&self, position: DockPosition) -> bool {
@@ -683,12 +637,12 @@ impl Panel for NotificationPanel {
);
}
- fn size(&self, cx: &gpui::WindowContext) -> f32 {
+ fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
- .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
+ .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
}
- fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+ fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
@@ -701,17 +655,14 @@ impl Panel for NotificationPanel {
}
}
- fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
- (settings::get::<NotificationPanelSettings>(cx).button
+ fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+ (NotificationPanelSettings::get_global(cx).button
&& self.notification_store.read(cx).notification_count() > 0)
- .then(|| "icons/bell.svg")
+ .then(|| Icon::Bell)
}
- fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
- (
- "Notification Panel".to_string(),
- Some(Box::new(ToggleFocus)),
- )
+ fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+ Some("Notification Panel")
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
@@ -723,20 +674,8 @@ impl Panel for NotificationPanel {
}
}
- fn should_change_position_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::DockPositionChanged)
- }
-
- fn should_close_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::Dismissed)
- }
-
- fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
- self.has_focus
- }
-
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, Event::Focus)
+ fn toggle_action(&self) -> Box<dyn gpui::Action> {
+ Box::new(ToggleFocus)
}
}
@@ -744,18 +683,14 @@ pub struct NotificationToast {
notification_id: u64,
actor: Option<Arc<User>>,
text: String,
- workspace: WeakViewHandle<Workspace>,
-}
-
-pub enum ToastEvent {
- Dismiss,
+ workspace: WeakView<Workspace>,
}
impl NotificationToast {
- fn focus_notification_panel(&self, cx: &mut AppContext) {
+ fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
let notification_id = self.notification_id;
- cx.defer(move |cx| {
+ cx.window_context().defer(move |cx| {
workspace
.update(cx, |workspace, cx| {
if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
@@ -772,91 +707,27 @@ impl NotificationToast {
}
}
-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> {
+impl Render for NotificationToast {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
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)
+ h_stack()
+ .id("notification_panel_toast")
+ .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
+ .child(Label::new(self.text.clone()))
+ .child(
+ IconButton::new("close", Icon::Close)
+ .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
+ )
+ .on_click(cx.listener(|this, _, cx| {
+ this.focus_notification_panel(cx);
+ cx.emit(DismissEvent);
+ }))
}
}
+impl EventEmitter<DismissEvent> for NotificationToast {}
+
fn format_timestamp(
mut timestamp: OffsetDateTime,
mut now: OffsetDateTime,
@@ -1,14 +1,15 @@
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,
+ img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
+ VisualContext as _, WindowHandle,
};
+use settings::Settings;
use std::sync::{Arc, Weak};
+use theme::ThemeSettings;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Button, Label};
use util::ResultExt;
use workspace::AppState;
@@ -19,21 +20,33 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
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);
+ window
+ .update(&mut cx, |_, cx| {
+ // todo!()
+ cx.remove_window();
+ })
+ .log_err();
}
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)
- });
+ let unique_screens = cx.update(|cx| cx.displays()).unwrap();
+ let window_size = gpui::Size {
+ width: px(380.),
+ height: px(64.),
+ };
- for screen in cx.platform().screens() {
+ for screen in unique_screens {
+ let options = notification_window_options(screen, window_size);
let window = cx
- .add_window(notification_window_options(screen, window_size), |_| {
- IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
- });
-
+ .open_window(options, |cx| {
+ cx.new_view(|_| {
+ IncomingCallNotification::new(
+ incoming_call.clone(),
+ app_state.clone(),
+ )
+ })
+ })
+ .unwrap();
notification_windows.push(window);
}
}
@@ -47,167 +60,104 @@ struct RespondToCall {
accept: bool,
}
-pub struct IncomingCallNotification {
+struct IncomingCallNotificationState {
call: IncomingCall,
app_state: Weak<AppState>,
}
-impl IncomingCallNotification {
+pub struct IncomingCallNotification {
+ state: Arc<IncomingCallNotificationState>,
+}
+impl IncomingCallNotificationState {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
- fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
+ fn respond(&self, accept: bool, cx: &mut AppContext) {
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);
+ let cx: &mut AppContext = cx;
+ cx.spawn(|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);
+ }
+ })
+ .log_err();
+ }
+ 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 IncomingCallNotification {
+ pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+ Self {
+ state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
+ }
}
}
-impl Entity for IncomingCallNotification {
- type Event = ();
-}
+impl Render for IncomingCallNotification {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ // TODO: Is there a better place for us to initialize the font?
+ let (ui_font, ui_font_size) = {
+ let theme_settings = ThemeSettings::get_global(cx);
+ (
+ theme_settings.ui_font.family.clone(),
+ theme_settings.ui_font_size.clone(),
+ )
+ };
-impl View for IncomingCallNotification {
- fn ui_name() -> &'static str {
- "IncomingCallNotification"
- }
+ cx.set_rem_size(ui_font_size);
- 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()
+ h_stack()
+ .font(ui_font)
+ .text_ui()
+ .justify_between()
+ .size_full()
+ .overflow_hidden()
+ .elevation_3(cx)
+ .p_2()
+ .gap_2()
+ .child(
+ img(self.state.call.calling_user.avatar_uri.clone())
+ .w_12()
+ .h_12()
+ .rounded_full(),
+ )
+ .child(v_stack().overflow_hidden().child(Label::new(format!(
+ "{} is sharing a project in Zed",
+ self.state.call.calling_user.github_login
+ ))))
+ .child(
+ v_stack()
+ .child(Button::new("accept", "Accept").render(cx).on_click({
+ let state = self.state.clone();
+ move |_, cx| state.respond(true, cx)
+ }))
+ .child(Button::new("decline", "Decline").render(cx).on_click({
+ let state = self.state.clone();
+ move |_, cx| state.respond(false, cx)
+ })),
+ )
}
}
@@ -2,13 +2,11 @@ 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 gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
+use settings::Settings;
use std::sync::{Arc, Weak};
+use theme::ThemeSettings;
+use ui::{h_stack, prelude::*, v_stack, Button, Label};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -21,38 +19,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
project_id,
worktree_root_names,
} => {
- let theme = &theme::current(cx).project_shared_notification;
- let window_size = vec2f(theme.window_width, theme.window_height);
+ let window_size = Size {
+ width: px(400.),
+ height: px(72.),
+ };
- for screen in cx.platform().screens() {
- let window =
- cx.add_window(notification_window_options(screen, window_size), |_| {
+ for screen in cx.displays() {
+ let options = notification_window_options(screen, window_size);
+ let window = cx.open_window(options, |cx| {
+ cx.new_view(|_| {
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);
+ window
+ .update(cx, |_, cx| {
+ // todo!()
+ cx.remove_window();
+ })
+ .ok();
}
}
}
+
room::Event::Left => {
for (_, windows) in notification_windows.drain() {
for window in windows {
- window.remove(cx);
+ window
+ .update(cx, |_, cx| {
+ // todo!()
+ cx.remove_window();
+ })
+ .ok();
}
}
}
@@ -101,117 +115,66 @@ impl ProjectSharedNotification {
});
}
}
+}
- 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(),
+impl Render for ProjectSharedNotification {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ // TODO: Is there a better place for us to initialize the font?
+ let (ui_font, ui_font_size) = {
+ let theme_settings = ThemeSettings::get_global(cx);
+ (
+ theme_settings.ui_font.family.clone(),
+ theme_settings.ui_font_size.clone(),
)
- .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 {}
+ cx.set_rem_size(ui_font_size);
- 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),
+ h_stack()
+ .font(ui_font)
+ .text_ui()
+ .justify_between()
+ .size_full()
+ .overflow_hidden()
+ .elevation_3(cx)
+ .p_2()
+ .gap_2()
+ .child(
+ img(self.owner.avatar_uri.clone())
+ .w_12()
+ .h_12()
+ .rounded_full(),
)
- .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),
+ .child(
+ v_stack()
+ .overflow_hidden()
+ .child(Label::new(self.owner.github_login.clone()))
+ .child(Label::new(format!(
+ "is sharing a project in Zed{}",
+ if self.worktree_root_names.is_empty() {
+ ""
+ } else {
+ ":"
+ }
+ )))
+ .children(if self.worktree_root_names.is_empty() {
+ None
+ } else {
+ Some(Label::new(self.worktree_root_names.join(", ")))
+ }),
+ )
+ .child(
+ v_stack()
+ .child(Button::new("open", "Open").on_click(cx.listener(
+ move |this, _event, cx| {
+ this.join(cx);
+ },
+ )))
+ .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+ move |this, _event, cx| {
+ this.dismiss(cx);
+ },
+ ))),
)
- .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()
}
}
@@ -1,28 +1,29 @@
use anyhow;
+use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: DockPosition,
- pub default_width: f32,
+ pub default_width: Pixels,
}
#[derive(Deserialize, Debug)]
pub struct ChatPanelSettings {
pub button: bool,
pub dock: DockPosition,
- pub default_width: f32,
+ pub default_width: Pixels,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
- pub default_width: f32,
+ pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -32,37 +33,37 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
-impl Setting for CollaborationPanelSettings {
+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],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
-impl Setting for ChatPanelSettings {
+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],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
-impl Setting for NotificationPanelSettings {
+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],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
@@ -1,81 +0,0 @@
-[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 = { package = "auto_update2", path = "../auto_update2" }
-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 = { package = "feedback2", path = "../feedback2" }
-fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
-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 = { package = "recent_projects2", path = "../recent_projects2" }
-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 = { package = "theme_selector2", path = "../theme_selector2" }
-vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
-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
@@ -1,448 +0,0 @@
-use 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, EditorEvent};
-use gpui::{
- actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
- IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
- VisualContext as _, WindowContext,
-};
-use project::Project;
-use std::{
- any::{Any, TypeId},
- sync::Arc,
-};
-use ui::{prelude::*, Label};
-use util::ResultExt;
-use workspace::{
- item::{FollowableItem, Item, ItemEvent, ItemHandle},
- register_followable_item,
- searchable::SearchableItemHandle,
- ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
-};
-
-actions!(collab, [Deploy]);
-
-pub fn init(cx: &mut AppContext) {
- register_followable_item::<ChannelView>(cx)
-}
-
-pub struct ChannelView {
- pub editor: View<Editor>,
- project: Model<Project>,
- channel_store: Model<ChannelStore>,
- channel_buffer: Model<ChannelBuffer>,
- remote_id: Option<ViewId>,
- _editor_event_subscription: Subscription,
-}
-
-impl ChannelView {
- pub fn open(
- channel_id: ChannelId,
- workspace: View<Workspace>,
- cx: &mut WindowContext,
- ) -> Task<Result<View<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: View<Pane>,
- workspace: View<Workspace>,
- cx: &mut WindowContext,
- ) -> Task<Result<View<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.new_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.entity_id(), SaveIntent::Skip, cx)
- .detach();
- pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
- }
- }
-
- view
- })
- })
- }
-
- pub fn new(
- project: Model<Project>,
- channel_store: Model<ChannelStore>,
- channel_buffer: Model<ChannelBuffer>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let buffer = channel_buffer.read(cx).buffer();
- let editor = cx.new_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: &EditorEvent, 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,
- _: Model<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::EditorEvent::TitleChanged);
- cx.notify()
- });
- }
- ChannelBufferEvent::BufferEdited => {
- if self.editor.read(cx).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 EventEmitter<EditorEvent> for ChannelView {}
-
-impl Render for ChannelView {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- self.editor.clone()
- }
-}
-
-impl FocusableView for ChannelView {
- fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
- self.editor.read(cx).focus_handle(cx)
- }
-}
-
-impl Item for ChannelView {
- type Event = EditorEvent;
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a View<Self>,
- _: &'a AppContext,
- ) -> Option<AnyView> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle.to_any())
- } else if type_id == TypeId::of::<Editor>() {
- Some(self.editor.to_any())
- } else {
- None
- }
- }
-
- fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
- 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)
- .color(if selected {
- Color::Default
- } else {
- Color::Muted
- })
- .into_any_element()
- }
-
- fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
- Some(cx.new_view(|cx| {
- 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, _: &View<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<Point<Pixels>> {
- self.editor.read(cx).pixel_position_of_cursor(cx)
- }
-
- fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
- Editor::to_item_events(event, f)
- }
-}
-
-impl FollowableItem for ChannelView {
- fn remote_id(&self) -> Option<workspace::ViewId> {
- self.remote_id
- }
-
- fn to_state_proto(&self, cx: &WindowContext) -> 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: View<workspace::Pane>,
- workspace: View<workspace::Workspace>,
- remote_id: workspace::ViewId,
- state: &mut Option<proto::view::Variant>,
- cx: &mut WindowContext,
- ) -> Option<gpui::Task<anyhow::Result<View<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
- }
- })?;
-
- if let Some(task) = task {
- task.await?;
- }
-
- Ok(this)
- }))
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &EditorEvent,
- update: &mut Option<proto::update_view::Variant>,
- cx: &WindowContext,
- ) -> bool {
- self.editor
- .read(cx)
- .add_event_to_update_proto(event, update, cx)
- }
-
- fn apply_update_proto(
- &mut self,
- project: &Model<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 is_project_item(&self, _cx: &WindowContext) -> bool {
- false
- }
-
- fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
- Editor::to_follow_event(event)
- }
-}
-
-struct ChannelBufferCollaborationHub(Model<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()
- }
-}
@@ -1,704 +0,0 @@
-use crate::{channel_view::ChannelView, is_channels_feature_enabled, 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, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
- ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
- Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
-};
-use language::LanguageRegistry;
-use menu::Confirm;
-use message_editor::MessageEditor;
-use project::Fs;
-use rich_text::RichText;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use std::sync::Arc;
-use theme::ActiveTheme as _;
-use time::{OffsetDateTime, UtcOffset};
-use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
- dock::{DockPosition, Panel, PanelEvent},
- Workspace,
-};
-
-mod message_editor;
-
-const MESSAGE_LOADING_THRESHOLD: usize = 50;
-const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|workspace: &mut Workspace, _| {
- workspace.register_action(|workspace, _: &ToggleFocus, cx| {
- workspace.toggle_panel_focus::<ChatPanel>(cx);
- });
- })
- .detach();
-}
-
-pub struct ChatPanel {
- client: Arc<Client>,
- channel_store: Model<ChannelStore>,
- languages: Arc<LanguageRegistry>,
- message_list: ListState,
- active_chat: Option<(Model<ChannelChat>, Subscription)>,
- input_editor: View<MessageEditor>,
- local_timezone: UtcOffset,
- fs: Arc<dyn Fs>,
- width: Option<Pixels>,
- active: bool,
- pending_serialization: Task<Option<()>>,
- subscriptions: Vec<gpui::Subscription>,
- workspace: WeakView<Workspace>,
- is_scrolled_to_bottom: bool,
- markdown_data: HashMap<ChannelMessageId, RichText>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedChatPanel {
- width: Option<Pixels>,
-}
-
-#[derive(Debug)]
-pub enum Event {
- DockPositionChanged,
- Focus,
- Dismissed,
-}
-
-actions!(chat_panel, [ToggleFocus]);
-
-impl ChatPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
- let fs = workspace.app_state().fs.clone();
- let client = workspace.app_state().client.clone();
- let channel_store = ChannelStore::global(cx);
- let languages = workspace.app_state().languages.clone();
-
- let input_editor = cx.new_view(|cx| {
- MessageEditor::new(
- languages.clone(),
- channel_store.clone(),
- cx.new_view(|cx| Editor::auto_height(4, cx)),
- cx,
- )
- });
-
- let workspace_handle = workspace.weak_handle();
-
- cx.new_view(|cx: &mut ViewContext<Self>| {
- let view = cx.view().downgrade();
- let message_list =
- ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
- if let Some(view) = view.upgrade() {
- view.update(cx, |view, cx| {
- view.render_message(ix, cx).into_any_element()
- })
- } else {
- div().into_any()
- }
- });
-
- message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
- if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
- this.load_more_messages(cx);
- }
- this.is_scrolled_to_bottom = event.visible_range.end == event.count;
- }));
-
- let mut this = Self {
- fs,
- client,
- channel_store,
- languages,
- message_list,
- active_chat: Default::default(),
- pending_serialization: Task::ready(None),
- input_editor,
- local_timezone: cx.local_timezone(),
- 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
- })
- }
-
- pub fn is_scrolled_to_bottom(&self) -> bool {
- self.is_scrolled_to_bottom
- }
-
- pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
- self.active_chat.as_ref().map(|(chat, _)| chat.clone())
- }
-
- pub fn load(
- workspace: WeakView<Workspace>,
- cx: AsyncWindowContext,
- ) -> Task<Result<View<Self>>> {
- cx.spawn(|mut cx| async move {
- let serialized_panel = if let Some(panel) = cx
- .background_executor()
- .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_executor().spawn(
- async move {
- KEY_VALUE_STORE
- .write_kvp(
- CHAT_PANEL_KEY.into(),
- serde_json::to_string(&SerializedChatPanel { width })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
- fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
- if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
- let channel_id = chat.read(cx).channel_id;
- {
- 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);
- cx.notify();
- }
- }
-
- fn channel_did_change(
- &mut self,
- _: Model<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<Self>) {
- 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 {
- v_stack()
- .full()
- .on_action(cx.listener(Self::send))
- .child(
- h_stack().z_index(1).child(
- TabBar::new("chat_header")
- .child(
- h_stack()
- .w_full()
- .h(rems(ui::Tab::HEIGHT_IN_REMS))
- .px_2()
- .child(Label::new(
- self.active_chat
- .as_ref()
- .and_then(|c| {
- Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
- })
- .unwrap_or_default(),
- )),
- )
- .end_child(
- IconButton::new("notes", Icon::File)
- .on_click(cx.listener(Self::open_notes))
- .tooltip(|cx| Tooltip::text("Open notes", cx)),
- )
- .end_child(
- IconButton::new("call", Icon::AudioOn)
- .on_click(cx.listener(Self::join_call))
- .tooltip(|cx| Tooltip::text("Join call", cx)),
- ),
- ),
- )
- .child(div().flex_grow().px_2().py_1().map(|this| {
- if self.active_chat.is_some() {
- this.child(list(self.message_list.clone()).full())
- } else {
- this
- }
- }))
- .child(
- div()
- .z_index(1)
- .p_2()
- .bg(cx.theme().colors().background)
- .child(self.input_editor.clone()),
- )
- .into_any()
- }
-
- fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let active_chat = &self.active_chat.as_ref().unwrap().0;
- let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
- active_chat.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 next_message =
- active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
-
- let is_continuation_from_previous = last_message.id != this_message.id
- && last_message.sender.id == this_message.sender.id;
- let is_continuation_to_next = this_message.id != next_message.id
- && this_message.sender.id == next_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_from_previous,
- is_continuation_to_next,
- is_admin,
- )
- });
-
- let _is_pending = message.is_pending();
- 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 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
- };
-
- let element_id: ElementId = match message.id {
- ChannelMessageId::Saved(id) => ("saved-message", id).into(),
- ChannelMessageId::Pending(id) => ("pending-message", id).into(),
- };
-
- v_stack()
- .w_full()
- .id(element_id)
- .relative()
- .overflow_hidden()
- .group("")
- .when(!is_continuation_from_previous, |this| {
- this.child(
- h_stack()
- .gap_2()
- .child(Avatar::new(message.sender.avatar_uri.clone()))
- .child(Label::new(message.sender.github_login.clone()))
- .child(
- Label::new(format_timestamp(
- message.timestamp,
- now,
- self.local_timezone,
- ))
- .color(Color::Muted),
- ),
- )
- })
- .when(!is_continuation_to_next, |this|
- // HACK: This should really be a margin, but margins seem to get collapsed.
- this.pb_2())
- .child(text.element("body".into(), cx))
- .child(
- div()
- .absolute()
- .top_1()
- .right_2()
- .w_8()
- .visible_on_hover("")
- .children(message_id_to_remove.map(|message_id| {
- IconButton::new(("remove", message_id), Icon::XCircle).on_click(
- cx.listener(move |this, _, cx| {
- this.remove_message(message_id, cx);
- }),
- )
- })),
- )
- }
-
- 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_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
- Button::new("sign-in", "Sign in to use chat")
- .on_click(cx.listener(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, |_, cx| {
- cx.focus_self();
- })
- .ok();
- }
- })
- .detach();
- }))
- .into_any_element()
- }
-
- 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, 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: px(0.0),
- });
- cx.notify();
- }
- })?;
- }
- }
-
- Ok(())
- })
- }
-
- fn open_notes(&mut self, _: &ClickEvent, 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() {
- ChannelView::open(channel_id, workspace, cx).detach();
- }
- }
- }
-
- fn join_call(&mut self, _: &ClickEvent, 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);
- }
- }
-}
-
-impl EventEmitter<Event> for ChatPanel {}
-
-impl Render for ChatPanel {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- div()
- .full()
- .child(if self.client.user_id().is_some() {
- self.render_channel(cx)
- } else {
- self.render_sign_in_prompt(cx)
- })
- .min_w(px(150.))
- }
-}
-
-impl FocusableView for ChatPanel {
- fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
- self.input_editor.read(cx).focus_handle(cx)
- }
-}
-
-impl Panel for ChatPanel {
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- ChatPanelSettings::get_global(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) -> Pixels {
- self.width
- .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, 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 persistent_name() -> &'static str {
- "ChatPanel"
- }
-
- fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
- Some(ui::Icon::MessageBubbles)
- }
-
- fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
- Some("Chat Panel")
- }
-
- fn toggle_action(&self) -> Box<dyn gpui::Action> {
- Box::new(ToggleFocus)
- }
-}
-
-impl EventEmitter<PanelEvent> for ChatPanel {}
-
-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())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::HighlightStyle;
- use pretty_assertions::assert_eq;
- use rich_text::Highlight;
- 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_uri: "avatar_fgh".into(),
- 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 {
- font_style: Some(gpui::FontStyle::Italic),
- ..Default::default()
- }
- .into()
- ),
- (ranges[1].clone(), Highlight::Mention),
- (
- ranges[2].clone(),
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- }
- .into()
- ),
- (ranges[3].clone(), Highlight::SelfMention)
- ]
- );
- }
-}
@@ -1,296 +0,0 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
-use client::UserId;
-use collections::HashMap;
-use editor::{AnchorRangeExt, Editor};
-use gpui::{
- AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
- ViewContext, WeakView,
-};
-use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
-use lazy_static::lazy_static;
-use project::search::SearchQuery;
-use std::{sync::Arc, time::Duration};
-use workspace::item::ItemHandle;
-
-const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
-
-lazy_static! {
- static ref MENTIONS_SEARCH: SearchQuery =
- SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
-}
-
-pub struct MessageEditor {
- pub editor: View<Editor>,
- channel_store: Model<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: Model<ChannelStore>,
- editor: View<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.spawn(|_, mut cx| async move {
- let markdown = markdown.await?;
- buffer.update(&mut cx, |buffer, cx| {
- buffer.set_language(Some(markdown), cx)
- })
- })
- .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<SharedString>,
- 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: Model<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_executor()
- .timer(MENTIONS_DEBOUNCE_INTERVAL)
- .await;
- Self::find_mentions(this, buffer, cx).await;
- }));
- }
- }
-
- async fn find_mentions(
- this: WeakView<MessageEditor>,
- buffer: BufferSnapshot,
- mut cx: AsyncWindowContext,
- ) {
- let (buffer, ranges) = cx
- .background_executor()
- .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, gpui::red().into(), cx)
- });
-
- this.mentions = mentioned_user_ids;
- this.mentions_task.take();
- })
- .ok();
- }
-
- pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
- self.editor.read(cx).focus_handle(cx)
- }
-}
-
-impl Render for MessageEditor {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- self.editor.to_any()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use client::{Client, User, UserStore};
- use gpui::{Context as _, TestAppContext, VisualContext as _};
- 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 language_registry = init_test(cx);
-
- let (editor, cx) = cx.add_window_view(|cx| {
- MessageEditor::new(
- language_registry,
- ChannelStore::global(cx),
- cx.new_view(|cx| Editor::auto_height(4, cx)),
- cx,
- )
- });
- cx.executor().run_until_parked();
-
- editor.update(cx, |editor, cx| {
- editor.set_members(
- vec![
- ChannelMembership {
- user: Arc::new(User {
- github_login: "a-b".into(),
- id: 101,
- avatar_uri: "avatar_a-b".into(),
- }),
- kind: proto::channel_member::Kind::Member,
- role: proto::ChannelRole::Member,
- },
- ChannelMembership {
- user: Arc::new(User {
- github_login: "C_D".into(),
- id: 102,
- avatar_uri: "avatar_C_D".into(),
- }),
- 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.executor().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) -> Arc<LanguageRegistry> {
- cx.update(|cx| {
- let http = FakeHttpClient::with_404_response();
- let client = Client::new(http.clone(), cx);
- let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
- theme::init(theme::LoadThemes::JustBase, 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()),
- )));
- language_registry
- }
-}
@@ -1,2539 +0,0 @@
-mod channel_modal;
-mod contact_finder;
-
-use self::channel_modal::ChannelModal;
-use crate::{
- channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
- CollaborationPanelSettings,
-};
-use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use client::{Client, Contact, User, UserStore};
-use contact_finder::ContactFinder;
-use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
- actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
- AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
- FocusHandle, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model,
- MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString,
- Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
-};
-use menu::{Cancel, Confirm, SelectNext, SelectPrev};
-use project::{Fs, Project};
-use rpc::proto::{self, PeerId};
-use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use smallvec::SmallVec;
-use std::{mem, sync::Arc};
-use theme::{ActiveTheme, ThemeSettings};
-use ui::prelude::*;
-use ui::{
- h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
- Label, ListHeader, ListItem, Tooltip,
-};
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
- dock::{DockPosition, Panel, PanelEvent},
- notifications::NotifyResultExt,
- Workspace,
-};
-
-actions!(
- collab_panel,
- [
- ToggleFocus,
- Remove,
- Secondary,
- CollapseSelectedChannel,
- ExpandSelectedChannel,
- StartMoveChannel,
- MoveSelected,
- InsertSpace,
- ]
-);
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-struct ChannelMoveClipboard {
- channel_id: ChannelId,
-}
-
-const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-
-pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|workspace: &mut Workspace, _| {
- workspace.register_action(|workspace, _: &ToggleFocus, cx| {
- workspace.toggle_panel_focus::<CollabPanel>(cx);
- });
- })
- .detach();
-}
-
-#[derive(Debug)]
-pub enum ChannelEditingState {
- Create {
- location: Option<ChannelId>,
- pending_name: Option<String>,
- },
- Rename {
- location: ChannelId,
- pending_name: Option<String>,
- },
-}
-
-impl ChannelEditingState {
- fn pending_name(&self) -> Option<String> {
- match self {
- ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
- ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
- }
- }
-}
-
-pub struct CollabPanel {
- width: Option<Pixels>,
- fs: Arc<dyn Fs>,
- focus_handle: FocusHandle,
- channel_clipboard: Option<ChannelMoveClipboard>,
- pending_serialization: Task<Option<()>>,
- context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
- list_state: ListState,
- filter_editor: View<Editor>,
- channel_name_editor: View<Editor>,
- channel_editing_state: Option<ChannelEditingState>,
- entries: Vec<ListEntry>,
- selection: Option<usize>,
- channel_store: Model<ChannelStore>,
- user_store: Model<UserStore>,
- client: Arc<Client>,
- project: Model<Project>,
- match_candidates: Vec<StringMatchCandidate>,
- subscriptions: Vec<Subscription>,
- collapsed_sections: Vec<Section>,
- collapsed_channels: Vec<ChannelId>,
- workspace: WeakView<Workspace>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedCollabPanel {
- width: Option<Pixels>,
- collapsed_channels: Option<Vec<u64>>,
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-enum Section {
- ActiveCall,
- Channels,
- ChannelInvites,
- ContactRequests,
- Contacts,
- Online,
- Offline,
-}
-
-#[derive(Clone, Debug)]
-enum ListEntry {
- Header(Section),
- CallParticipant {
- user: Arc<User>,
- peer_id: Option<PeerId>,
- is_pending: bool,
- },
- ParticipantProject {
- project_id: u64,
- worktree_root_names: Vec<String>,
- host_user_id: u64,
- is_last: bool,
- },
- ParticipantScreen {
- peer_id: Option<PeerId>,
- is_last: bool,
- },
- IncomingRequest(Arc<User>),
- OutgoingRequest(Arc<User>),
- ChannelInvite(Arc<Channel>),
- Channel {
- channel: Arc<Channel>,
- depth: usize,
- has_children: bool,
- },
- ChannelNotes {
- channel_id: ChannelId,
- },
- ChannelChat {
- channel_id: ChannelId,
- },
- ChannelEditor {
- depth: usize,
- },
- Contact {
- contact: Arc<Contact>,
- calling: bool,
- },
- ContactPlaceholder,
-}
-
-impl CollabPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
- cx.new_view(|cx| {
- let filter_editor = cx.new_view(|cx| {
- let mut editor = Editor::single_line(cx);
- editor.set_placeholder_text("Filter...", cx);
- editor
- });
-
- cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
- if let editor::EditorEvent::BufferEdited = event {
- let query = this.filter_editor.read(cx).text(cx);
- if !query.is_empty() {
- this.selection.take();
- }
- this.update_entries(true, cx);
- if !query.is_empty() {
- this.selection = this
- .entries
- .iter()
- .position(|entry| !matches!(entry, ListEntry::Header(_)));
- }
- }
- })
- .detach();
-
- let channel_name_editor = cx.new_view(|cx| Editor::single_line(cx));
-
- cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
- if let editor::EditorEvent::Blurred = event {
- if let Some(state) = &this.channel_editing_state {
- if state.pending_name().is_some() {
- return;
- }
- }
- this.take_editing_state(cx);
- this.update_entries(false, cx);
- cx.notify();
- }
- })
- .detach();
-
- let view = cx.view().downgrade();
- let list_state =
- ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
- if let Some(view) = view.upgrade() {
- view.update(cx, |view, cx| view.render_list_entry(ix, cx))
- } else {
- div().into_any()
- }
- });
-
- let mut this = Self {
- width: None,
- focus_handle: cx.focus_handle(),
- channel_clipboard: None,
- fs: workspace.app_state().fs.clone(),
- pending_serialization: Task::ready(None),
- context_menu: None,
- list_state,
- channel_name_editor,
- filter_editor,
- entries: Vec::default(),
- channel_editing_state: None,
- selection: None,
- channel_store: ChannelStore::global(cx),
- user_store: workspace.user_store().clone(),
- project: workspace.project().clone(),
- subscriptions: Vec::default(),
- match_candidates: Vec::default(),
- collapsed_sections: vec![Section::Offline],
- collapsed_channels: Vec::default(),
- workspace: workspace.weak_handle(),
- client: workspace.app_state().client.clone(),
- };
-
- this.update_entries(false, cx);
-
- // Update the dock position when the setting changes.
- 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(PanelEvent::ChangePosition);
- }
- cx.notify();
- },
- ));
-
- let active_call = ActiveCall::global(cx);
- this.subscriptions
- .push(cx.observe(&this.user_store, |this, _, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions
- .push(cx.observe(&this.channel_store, |this, _, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions
- .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
- this.subscriptions
- .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions.push(cx.subscribe(
- &this.channel_store,
- |this, _channel_store, e, cx| match e {
- ChannelEvent::ChannelCreated(channel_id)
- | ChannelEvent::ChannelRenamed(channel_id) => {
- if this.take_editing_state(cx) {
- this.update_entries(false, cx);
- this.selection = this.entries.iter().position(|entry| {
- if let ListEntry::Channel { channel, .. } = entry {
- channel.id == *channel_id
- } else {
- false
- }
- });
- }
- }
- },
- ));
-
- this
- })
- }
-
- pub async fn load(
- workspace: WeakView<Workspace>,
- mut cx: AsyncWindowContext,
- ) -> anyhow::Result<View<Self>> {
- let serialized_panel = cx
- .background_executor()
- .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
- .await
- .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
- .log_err()
- .flatten()
- .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
- .transpose()
- .log_err()
- .flatten();
-
- workspace.update(&mut cx, |workspace, cx| {
- let panel = CollabPanel::new(workspace, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width;
- panel.collapsed_channels = serialized_panel
- .collapsed_channels
- .unwrap_or_else(|| Vec::new());
- cx.notify();
- });
- }
- panel
- })
- }
-
- fn serialize(&mut self, cx: &mut ViewContext<Self>) {
- let width = self.width;
- let collapsed_channels = self.collapsed_channels.clone();
- self.pending_serialization = cx.background_executor().spawn(
- async move {
- KEY_VALUE_STORE
- .write_kvp(
- COLLABORATION_PANEL_KEY.into(),
- serde_json::to_string(&SerializedCollabPanel {
- width,
- collapsed_channels: Some(collapsed_channels),
- })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
- fn scroll_to_item(&mut self, ix: usize) {
- self.list_state.scroll_to_reveal_item(ix)
- }
-
- fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- let user_store = self.user_store.read(cx);
- let query = self.filter_editor.read(cx).text(cx);
- let executor = cx.background_executor().clone();
-
- let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
- let old_entries = mem::take(&mut self.entries);
- let mut scroll_to_top = false;
-
- if let Some(room) = ActiveCall::global(cx).read(cx).room() {
- self.entries.push(ListEntry::Header(Section::ActiveCall));
- if !old_entries
- .iter()
- .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
- {
- scroll_to_top = true;
- }
-
- if !self.collapsed_sections.contains(&Section::ActiveCall) {
- let room = room.read(cx);
-
- if let Some(channel_id) = room.channel_id() {
- self.entries.push(ListEntry::ChannelNotes { channel_id });
- self.entries.push(ListEntry::ChannelChat { channel_id })
- }
-
- // Populate the active user.
- if let Some(user) = user_store.current_user() {
- self.match_candidates.clear();
- self.match_candidates.push(StringMatchCandidate {
- id: 0,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- });
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- if !matches.is_empty() {
- let user_id = user.id;
- self.entries.push(ListEntry::CallParticipant {
- user,
- peer_id: None,
- is_pending: false,
- });
- let mut projects = room.local_participant().projects.iter().peekable();
- while let Some(project) = projects.next() {
- self.entries.push(ListEntry::ParticipantProject {
- project_id: project.id,
- worktree_root_names: project.worktree_root_names.clone(),
- host_user_id: user_id,
- is_last: projects.peek().is_none() && !room.is_screen_sharing(),
- });
- }
- if room.is_screen_sharing() {
- self.entries.push(ListEntry::ParticipantScreen {
- peer_id: None,
- is_last: true,
- });
- }
- }
- }
-
- // Populate remote participants.
- self.match_candidates.clear();
- self.match_candidates
- .extend(room.remote_participants().iter().map(|(_, participant)| {
- StringMatchCandidate {
- id: participant.user.id as usize,
- string: participant.user.github_login.clone(),
- char_bag: participant.user.github_login.chars().collect(),
- }
- }));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- for mat in matches {
- let user_id = mat.candidate_id as u64;
- let participant = &room.remote_participants()[&user_id];
- self.entries.push(ListEntry::CallParticipant {
- user: participant.user.clone(),
- peer_id: Some(participant.peer_id),
- is_pending: false,
- });
- let mut projects = participant.projects.iter().peekable();
- while let Some(project) = projects.next() {
- self.entries.push(ListEntry::ParticipantProject {
- project_id: project.id,
- worktree_root_names: project.worktree_root_names.clone(),
- host_user_id: participant.user.id,
- is_last: projects.peek().is_none()
- && participant.video_tracks.is_empty(),
- });
- }
- if !participant.video_tracks.is_empty() {
- self.entries.push(ListEntry::ParticipantScreen {
- peer_id: Some(participant.peer_id),
- is_last: true,
- });
- }
- }
-
- // Populate pending participants.
- self.match_candidates.clear();
- self.match_candidates
- .extend(room.pending_participants().iter().enumerate().map(
- |(id, participant)| StringMatchCandidate {
- id,
- string: participant.github_login.clone(),
- char_bag: participant.github_login.chars().collect(),
- },
- ));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- self.entries
- .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
- user: room.pending_participants()[mat.candidate_id].clone(),
- peer_id: None,
- is_pending: true,
- }));
- }
- }
-
- let mut request_entries = Vec::new();
-
- if cx.has_flag::<ChannelsAlpha>() {
- self.entries.push(ListEntry::Header(Section::Channels));
-
- if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_store.ordered_channels().enumerate().map(
- |(ix, (_, channel))| StringMatchCandidate {
- id: ix,
- string: channel.name.clone().into(),
- char_bag: channel.name.chars().collect(),
- },
- ));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- if let Some(state) = &self.channel_editing_state {
- if matches!(state, ChannelEditingState::Create { location: None, .. }) {
- self.entries.push(ListEntry::ChannelEditor { depth: 0 });
- }
- }
- let mut collapse_depth = None;
- for mat in matches {
- let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
- let depth = channel.parent_path.len();
-
- if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
- collapse_depth = Some(depth);
- } else if let Some(collapsed_depth) = collapse_depth {
- if depth > collapsed_depth {
- continue;
- }
- if self.is_channel_collapsed(channel.id) {
- collapse_depth = Some(depth);
- } else {
- collapse_depth = None;
- }
- }
-
- let has_children = channel_store
- .channel_at_index(mat.candidate_id + 1)
- .map_or(false, |next_channel| {
- next_channel.parent_path.ends_with(&[channel.id])
- });
-
- match &self.channel_editing_state {
- Some(ChannelEditingState::Create {
- location: parent_id,
- ..
- }) if *parent_id == Some(channel.id) => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children: false,
- });
- self.entries
- .push(ListEntry::ChannelEditor { depth: depth + 1 });
- }
- Some(ChannelEditingState::Rename {
- location: parent_id,
- ..
- }) if parent_id == &channel.id => {
- self.entries.push(ListEntry::ChannelEditor { depth });
- }
- _ => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children,
- });
- }
- }
- }
- }
-
- let channel_invites = channel_store.channel_invitations();
- if !channel_invites.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
- StringMatchCandidate {
- id: ix,
- string: channel.name.clone().into(),
- char_bag: channel.name.chars().collect(),
- }
- }));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(matches.iter().map(|mat| {
- ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
- }));
-
- if !request_entries.is_empty() {
- self.entries
- .push(ListEntry::Header(Section::ChannelInvites));
- if !self.collapsed_sections.contains(&Section::ChannelInvites) {
- self.entries.append(&mut request_entries);
- }
- }
- }
- }
-
- self.entries.push(ListEntry::Header(Section::Contacts));
-
- request_entries.clear();
- let incoming = user_store.incoming_contact_requests();
- if !incoming.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- incoming
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
- );
- }
-
- let outgoing = user_store.outgoing_contact_requests();
- if !outgoing.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- outgoing
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
- );
- }
-
- if !request_entries.is_empty() {
- self.entries
- .push(ListEntry::Header(Section::ContactRequests));
- if !self.collapsed_sections.contains(&Section::ContactRequests) {
- self.entries.append(&mut request_entries);
- }
- }
-
- let contacts = user_store.contacts();
- if !contacts.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- contacts
- .iter()
- .enumerate()
- .map(|(ix, contact)| StringMatchCandidate {
- id: ix,
- string: contact.user.github_login.clone(),
- char_bag: contact.user.github_login.chars().collect(),
- }),
- );
-
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
-
- let (online_contacts, offline_contacts) = matches
- .iter()
- .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
- for (matches, section) in [
- (online_contacts, Section::Online),
- (offline_contacts, Section::Offline),
- ] {
- if !matches.is_empty() {
- self.entries.push(ListEntry::Header(section));
- if !self.collapsed_sections.contains(§ion) {
- let active_call = &ActiveCall::global(cx).read(cx);
- for mat in matches {
- let contact = &contacts[mat.candidate_id];
- self.entries.push(ListEntry::Contact {
- contact: contact.clone(),
- calling: active_call.pending_invites().contains(&contact.user.id),
- });
- }
- }
- }
- }
- }
-
- if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
- self.entries.push(ListEntry::ContactPlaceholder);
- }
-
- if select_same_item {
- if let Some(prev_selected_entry) = prev_selected_entry {
- self.selection.take();
- for (ix, entry) in self.entries.iter().enumerate() {
- if *entry == prev_selected_entry {
- self.selection = Some(ix);
- break;
- }
- }
- }
- } else {
- self.selection = self.selection.and_then(|prev_selection| {
- if self.entries.is_empty() {
- None
- } else {
- Some(prev_selection.min(self.entries.len() - 1))
- }
- });
- }
-
- let old_scroll_top = self.list_state.logical_scroll_top();
- self.list_state.reset(self.entries.len());
-
- if scroll_to_top {
- self.list_state.scroll_to(ListOffset::default());
- } else {
- // Attempt to maintain the same scroll position.
- if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
- let new_scroll_top = self
- .entries
- .iter()
- .position(|entry| entry == old_top_entry)
- .map(|item_ix| ListOffset {
- item_ix,
- offset_in_item: old_scroll_top.offset_in_item,
- })
- .or_else(|| {
- let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
- let item_ix = self
- .entries
- .iter()
- .position(|entry| entry == entry_after_old_top)?;
- Some(ListOffset {
- item_ix,
- offset_in_item: Pixels::ZERO,
- })
- })
- .or_else(|| {
- let entry_before_old_top =
- old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
- let item_ix = self
- .entries
- .iter()
- .position(|entry| entry == entry_before_old_top)?;
- Some(ListOffset {
- item_ix,
- offset_in_item: Pixels::ZERO,
- })
- });
-
- self.list_state
- .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
- }
- }
-
- cx.notify();
- }
-
- fn render_call_participant(
- &self,
- user: &Arc<User>,
- peer_id: Option<PeerId>,
- is_pending: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> ListItem {
- let is_current_user =
- self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
- let tooltip = format!("Follow {}", user.github_login);
-
- ListItem::new(SharedString::from(user.github_login.clone()))
- .start_slot(Avatar::new(user.avatar_uri.clone()))
- .child(Label::new(user.github_login.clone()))
- .selected(is_selected)
- .end_slot(if is_pending {
- Label::new("Calling").color(Color::Muted).into_any_element()
- } else if is_current_user {
- IconButton::new("leave-call", Icon::Exit)
- .style(ButtonStyle::Subtle)
- .on_click(move |_, cx| Self::leave_call(cx))
- .tooltip(|cx| Tooltip::text("Leave Call", cx))
- .into_any_element()
- } else {
- div().into_any_element()
- })
- .when_some(peer_id, |this, peer_id| {
- this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
- .on_click(cx.listener(move |this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
- .ok();
- }))
- })
- }
-
- fn render_participant_project(
- &self,
- project_id: u64,
- worktree_root_names: &[String],
- host_user_id: u64,
- is_last: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- let project_name: SharedString = if worktree_root_names.is_empty() {
- "untitled".to_string()
- } else {
- worktree_root_names.join(", ")
- }
- .into();
-
- ListItem::new(project_id as usize)
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- let app_state = workspace.app_state().clone();
- workspace::join_remote_project(project_id, host_user_id, app_state, cx)
- .detach_and_log_err(cx);
- })
- .ok();
- }))
- .start_slot(
- h_stack()
- .gap_1()
- .child(render_tree_branch(is_last, cx))
- .child(IconButton::new(0, Icon::Folder)),
- )
- .child(Label::new(project_name.clone()))
- .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
- }
-
- fn render_participant_screen(
- &self,
- peer_id: Option<PeerId>,
- is_last: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
-
- ListItem::new(("screen", id))
- .selected(is_selected)
- .start_slot(
- h_stack()
- .gap_1()
- .child(render_tree_branch(is_last, cx))
- .child(IconButton::new(0, Icon::Screen)),
- )
- .child(Label::new("Screen"))
- .when_some(peer_id, |this, _| {
- this.on_click(cx.listener(move |this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- workspace.open_shared_screen(peer_id.unwrap(), cx)
- })
- .ok();
- }))
- .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
- })
- }
-
- fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(_) = self.channel_editing_state.take() {
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.set_text("", cx);
- });
- true
- } else {
- false
- }
- }
-
- fn render_channel_notes(
- &self,
- channel_id: ChannelId,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- ListItem::new("channel-notes")
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| {
- this.open_channel_notes(channel_id, cx);
- }))
- .start_slot(
- h_stack()
- .gap_1()
- .child(render_tree_branch(false, cx))
- .child(IconButton::new(0, Icon::File)),
- )
- .child(div().h_7().w_full().child(Label::new("notes")))
- .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
- }
-
- fn render_channel_chat(
- &self,
- channel_id: ChannelId,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- ListItem::new("channel-chat")
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| {
- this.join_channel_chat(channel_id, cx);
- }))
- .start_slot(
- h_stack()
- .gap_1()
- .child(render_tree_branch(false, cx))
- .child(IconButton::new(0, Icon::MessageBubbles)),
- )
- .child(Label::new("chat"))
- .tooltip(move |cx| Tooltip::text("Open Chat", cx))
- }
-
- fn has_subchannels(&self, ix: usize) -> bool {
- self.entries.get(ix).map_or(false, |entry| {
- if let ListEntry::Channel { has_children, .. } = entry {
- *has_children
- } else {
- false
- }
- })
- }
-
- fn deploy_channel_context_menu(
- &mut self,
- position: Point<Pixels>,
- channel_id: ChannelId,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) {
- let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
- self.channel_store
- .read(cx)
- .channel_for_id(clipboard.channel_id)
- .map(|channel| channel.name.clone())
- });
- let this = cx.view().clone();
-
- let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
- if self.has_subchannels(ix) {
- let expand_action_name = if self.is_channel_collapsed(channel_id) {
- "Expand Subchannels"
- } else {
- "Collapse Subchannels"
- };
- context_menu = context_menu.entry(
- expand_action_name,
- None,
- cx.handler_for(&this, move |this, cx| {
- this.toggle_channel_collapsed(channel_id, cx)
- }),
- );
- }
-
- context_menu = context_menu
- .entry(
- "Open Notes",
- None,
- cx.handler_for(&this, move |this, cx| {
- this.open_channel_notes(channel_id, cx)
- }),
- )
- .entry(
- "Open Chat",
- None,
- cx.handler_for(&this, move |this, cx| {
- this.join_channel_chat(channel_id, cx)
- }),
- )
- .entry(
- "Copy Channel Link",
- None,
- cx.handler_for(&this, move |this, cx| {
- this.copy_channel_link(channel_id, cx)
- }),
- );
-
- if self.channel_store.read(cx).is_channel_admin(channel_id) {
- context_menu = context_menu
- .separator()
- .entry(
- "New Subchannel",
- None,
- cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
- )
- .entry(
- "Rename",
- None,
- cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
- )
- .entry(
- "Move this channel",
- None,
- cx.handler_for(&this, move |this, cx| {
- this.start_move_channel(channel_id, cx)
- }),
- );
-
- if let Some(channel_name) = clipboard_channel_name {
- context_menu = context_menu.separator().entry(
- format!("Move '#{}' here", channel_name),
- None,
- cx.handler_for(&this, move |this, cx| {
- this.move_channel_on_clipboard(channel_id, cx)
- }),
- );
- }
-
- context_menu = context_menu
- .separator()
- .entry(
- "Invite Members",
- None,
- cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
- )
- .entry(
- "Manage Members",
- None,
- cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
- )
- .entry(
- "Delete",
- None,
- cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
- );
- }
-
- context_menu
- });
-
- cx.focus_view(&context_menu);
- let subscription =
- cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
- if this.context_menu.as_ref().is_some_and(|context_menu| {
- context_menu.0.focus_handle(cx).contains_focused(cx)
- }) {
- cx.focus_self();
- }
- this.context_menu.take();
- cx.notify();
- });
- self.context_menu = Some((context_menu, position, subscription));
-
- cx.notify();
- }
-
- fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- if self.take_editing_state(cx) {
- cx.focus_view(&self.filter_editor);
- } else {
- self.filter_editor.update(cx, |editor, cx| {
- if editor.buffer().read(cx).len(cx) > 0 {
- editor.set_text("", cx);
- }
- });
- }
-
- self.update_entries(false, cx);
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- let ix = self.selection.map_or(0, |ix| ix + 1);
- if ix < self.entries.len() {
- self.selection = Some(ix);
- }
-
- if let Some(ix) = self.selection {
- self.scroll_to_item(ix)
- }
- cx.notify();
- }
-
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- let ix = self.selection.take().unwrap_or(0);
- if ix > 0 {
- self.selection = Some(ix - 1);
- }
-
- if let Some(ix) = self.selection {
- self.scroll_to_item(ix)
- }
- cx.notify();
- }
-
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- if self.confirm_channel_edit(cx) {
- return;
- }
-
- if let Some(selection) = self.selection {
- if let Some(entry) = self.entries.get(selection) {
- match entry {
- ListEntry::Header(section) => match section {
- Section::ActiveCall => Self::leave_call(cx),
- Section::Channels => self.new_root_channel(cx),
- Section::Contacts => self.toggle_contact_finder(cx),
- Section::ContactRequests
- | Section::Online
- | Section::Offline
- | Section::ChannelInvites => {
- self.toggle_section_expanded(*section, cx);
- }
- },
- ListEntry::Contact { contact, calling } => {
- if contact.online && !contact.busy && !calling {
- self.call(contact.user.id, cx);
- }
- }
- ListEntry::ParticipantProject {
- project_id,
- host_user_id,
- ..
- } => {
- if let Some(workspace) = self.workspace.upgrade() {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(
- *project_id,
- *host_user_id,
- app_state,
- cx,
- )
- .detach_and_log_err(cx);
- }
- }
- ListEntry::ParticipantScreen { peer_id, .. } => {
- let Some(peer_id) = peer_id else {
- return;
- };
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(*peer_id, cx)
- });
- }
- }
- ListEntry::Channel { channel, .. } => {
- let is_active = maybe!({
- let call_channel = ActiveCall::global(cx)
- .read(cx)
- .room()?
- .read(cx)
- .channel_id()?;
-
- Some(call_channel == channel.id)
- })
- .unwrap_or(false);
- if is_active {
- self.open_channel_notes(channel.id, cx)
- } else {
- self.join_channel(channel.id, cx)
- }
- }
- ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
- ListEntry::CallParticipant { user, peer_id, .. } => {
- if Some(user) == self.user_store.read(cx).current_user().as_ref() {
- Self::leave_call(cx);
- } else if let Some(peer_id) = peer_id {
- self.workspace
- .update(cx, |workspace, cx| workspace.follow(*peer_id, cx))
- .ok();
- }
- }
- ListEntry::IncomingRequest(user) => {
- self.respond_to_contact_request(user.id, true, cx)
- }
- ListEntry::ChannelInvite(channel) => {
- self.respond_to_channel_invite(channel.id, true, cx)
- }
- ListEntry::ChannelNotes { channel_id } => {
- self.open_channel_notes(*channel_id, cx)
- }
- ListEntry::ChannelChat { channel_id } => {
- self.join_channel_chat(*channel_id, cx)
- }
-
- ListEntry::OutgoingRequest(_) => {}
- ListEntry::ChannelEditor { .. } => {}
- }
- }
- }
- }
-
- fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
- if self.channel_editing_state.is_some() {
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.insert(" ", cx);
- });
- }
- }
-
- fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
- if let Some(editing_state) = &mut self.channel_editing_state {
- match editing_state {
- ChannelEditingState::Create {
- location,
- pending_name,
- ..
- } => {
- if pending_name.is_some() {
- return false;
- }
- let channel_name = self.channel_name_editor.read(cx).text(cx);
-
- *pending_name = Some(channel_name.clone());
-
- self.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.create_channel(&channel_name, *location, cx)
- })
- .detach();
- cx.notify();
- }
- ChannelEditingState::Rename {
- location,
- pending_name,
- } => {
- if pending_name.is_some() {
- return false;
- }
- let channel_name = self.channel_name_editor.read(cx).text(cx);
- *pending_name = Some(channel_name.clone());
-
- self.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.rename(*location, &channel_name, cx)
- })
- .detach();
- cx.notify();
- }
- }
- cx.focus_self();
- true
- } else {
- false
- }
- }
-
- fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
- if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
- self.collapsed_sections.remove(ix);
- } else {
- self.collapsed_sections.push(section);
- }
- self.update_entries(false, cx);
- }
-
- fn collapse_selected_channel(
- &mut self,
- _: &CollapseSelectedChannel,
- cx: &mut ViewContext<Self>,
- ) {
- let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
- return;
- };
-
- if self.is_channel_collapsed(channel_id) {
- return;
- }
-
- self.toggle_channel_collapsed(channel_id, cx);
- }
-
- fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
- let Some(id) = self.selected_channel().map(|channel| channel.id) else {
- return;
- };
-
- if !self.is_channel_collapsed(id) {
- return;
- }
-
- self.toggle_channel_collapsed(id, cx)
- }
-
- // fn toggle_channel_collapsed_action(
- // &mut self,
- // action: &ToggleCollapse,
- // cx: &mut ViewContext<Self>,
- // ) {
- // self.toggle_channel_collapsed(action.location, cx);
- // }
-
- fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- match self.collapsed_channels.binary_search(&channel_id) {
- Ok(ix) => {
- self.collapsed_channels.remove(ix);
- }
- Err(ix) => {
- self.collapsed_channels.insert(ix, channel_id);
- }
- };
- self.serialize(cx);
- self.update_entries(true, cx);
- cx.notify();
- cx.focus_self();
- }
-
- fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
- self.collapsed_channels.binary_search(&channel_id).is_ok()
- }
-
- fn leave_call(cx: &mut WindowContext) {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- }
-
- fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- workspace.toggle_modal(cx, |cx| {
- let mut finder = ContactFinder::new(self.user_store.clone(), cx);
- finder.set_query(self.filter_editor.read(cx).text(cx), cx);
- finder
- });
- });
- }
- }
-
- fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
- self.channel_editing_state = Some(ChannelEditingState::Create {
- location: None,
- pending_name: None,
- });
- self.update_entries(false, cx);
- self.select_channel_editor();
- cx.focus_view(&self.channel_name_editor);
- cx.notify();
- }
-
- fn select_channel_editor(&mut self) {
- self.selection = self.entries.iter().position(|entry| match entry {
- ListEntry::ChannelEditor { .. } => true,
- _ => false,
- });
- }
-
- fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- self.collapsed_channels
- .retain(|channel| *channel != channel_id);
- self.channel_editing_state = Some(ChannelEditingState::Create {
- location: Some(channel_id),
- pending_name: None,
- });
- self.update_entries(false, cx);
- self.select_channel_editor();
- cx.focus_view(&self.channel_name_editor);
- cx.notify();
- }
-
- fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
- }
-
- fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
- }
-
- fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
- if let Some(channel) = self.selected_channel() {
- self.remove_channel(channel.id, cx)
- }
- }
-
- fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
- if let Some(channel) = self.selected_channel() {
- self.rename_channel(channel.id, cx);
- }
- }
-
- fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- if !channel_store.is_channel_admin(channel_id) {
- return;
- }
- if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
- self.channel_editing_state = Some(ChannelEditingState::Rename {
- location: channel_id,
- pending_name: None,
- });
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.set_text(channel.name.clone(), cx);
- editor.select_all(&Default::default(), cx);
- });
- cx.focus_view(&self.channel_name_editor);
- self.update_entries(false, cx);
- self.select_channel_editor();
- }
- }
-
- fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
- self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
- }
-
- fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
- if let Some(channel) = self.selected_channel() {
- self.start_move_channel(channel.id, cx);
- }
- }
-
- fn move_channel_on_clipboard(
- &mut self,
- to_channel_id: ChannelId,
- cx: &mut ViewContext<CollabPanel>,
- ) {
- if let Some(clipboard) = self.channel_clipboard.take() {
- self.channel_store.update(cx, |channel_store, cx| {
- channel_store
- .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
- .detach_and_log_err(cx)
- })
- }
- }
-
- fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade() {
- ChannelView::open(channel_id, workspace, cx).detach();
- }
- }
-
- fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
- let Some(channel) = self.selected_channel() else {
- return;
- };
- let Some(bounds) = self
- .selection
- .and_then(|ix| self.list_state.bounds_for_item(ix))
- else {
- return;
- };
-
- self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
- cx.stop_propagation();
- }
-
- fn selected_channel(&self) -> Option<&Arc<Channel>> {
- self.selection
- .and_then(|ix| self.entries.get(ix))
- .and_then(|entry| match entry {
- ListEntry::Channel { channel, .. } => Some(channel),
- _ => None,
- })
- }
-
- fn show_channel_modal(
- &mut self,
- channel_id: ChannelId,
- mode: channel_modal::Mode,
- cx: &mut ViewContext<Self>,
- ) {
- let workspace = self.workspace.clone();
- let user_store = self.user_store.clone();
- let channel_store = self.channel_store.clone();
- let members = self.channel_store.update(cx, |channel_store, cx| {
- channel_store.get_channel_member_details(channel_id, cx)
- });
-
- cx.spawn(|_, mut cx| async move {
- let members = members.await?;
- workspace.update(&mut cx, |workspace, cx| {
- workspace.toggle_modal(cx, |cx| {
- ChannelModal::new(
- user_store.clone(),
- channel_store.clone(),
- channel_id,
- mode,
- members,
- cx,
- )
- });
- })
- })
- .detach();
- }
-
- fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.clone();
- if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
- let prompt_message = format!(
- "Are you sure you want to remove the channel \"{}\"?",
- channel.name
- );
- let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
- cx.spawn(|this, mut cx| async move {
- if answer.await? == 0 {
- channel_store
- .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
- .await
- .notify_async_err(&mut cx);
- this.update(&mut cx, |_, cx| cx.focus_self()).ok();
- }
- anyhow::Ok(())
- })
- .detach();
- }
- }
-
- fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
- let user_store = self.user_store.clone();
- let prompt_message = format!(
- "Are you sure you want to remove \"{}\" from your contacts?",
- github_login
- );
- let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
- cx.spawn(|_, mut cx| async move {
- if answer.await? == 0 {
- user_store
- .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
- .await
- .notify_async_err(&mut cx);
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
-
- fn respond_to_contact_request(
- &mut self,
- user_id: u64,
- accept: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.user_store
- .update(cx, |store, cx| {
- store.respond_to_contact_request(user_id, accept, cx)
- })
- .detach_and_log_err(cx);
- }
-
- fn respond_to_channel_invite(
- &mut self,
- channel_id: u64,
- accept: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.channel_store
- .update(cx, |store, cx| {
- store.respond_to_channel_invite(channel_id, accept, cx)
- })
- .detach();
- }
-
- fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
- ActiveCall::global(cx)
- .update(cx, |call, cx| {
- call.invite(recipient_user_id, Some(self.project.clone()), cx)
- })
- .detach_and_log_err(cx);
- }
-
- fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
- let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
- return;
- };
- workspace::join_channel(
- channel_id,
- workspace.read(cx).app_state().clone(),
- Some(handle),
- cx,
- )
- .detach_and_log_err(cx)
- }
-
- fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
- cx.window_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, None, cx)
- .detach_and_log_err(cx);
- });
- }
- });
- });
- }
-
- fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- let Some(channel) = channel_store.channel_for_id(channel_id) else {
- return;
- };
- let item = ClipboardItem::new(channel.link());
- cx.write_to_clipboard(item)
- }
-
- fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
- let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
-
- v_stack()
- .gap_6()
- .p_4()
- .child(Label::new(collab_blurb))
- .child(
- v_stack()
- .gap_2()
- .child(
- Button::new("sign_in", "Sign in")
- .icon_color(Color::Muted)
- .icon(Icon::Github)
- .icon_position(IconPosition::Start)
- .style(ButtonStyle::Filled)
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- let client = this.client.clone();
- cx.spawn(|_, mut cx| async move {
- client
- .authenticate_and_connect(true, &cx)
- .await
- .notify_async_err(&mut cx);
- })
- .detach()
- })),
- )
- .child(
- div().flex().w_full().items_center().child(
- Label::new("Sign in to enable collaboration.")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
- )
- }
-
- fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
- let entry = &self.entries[ix];
-
- let is_selected = self.selection == Some(ix);
- match entry {
- ListEntry::Header(section) => {
- let is_collapsed = self.collapsed_sections.contains(section);
- self.render_header(*section, is_selected, is_collapsed, cx)
- .into_any_element()
- }
- ListEntry::Contact { contact, calling } => self
- .render_contact(contact, *calling, is_selected, cx)
- .into_any_element(),
- ListEntry::ContactPlaceholder => self
- .render_contact_placeholder(is_selected, cx)
- .into_any_element(),
- ListEntry::IncomingRequest(user) => self
- .render_contact_request(user, true, is_selected, cx)
- .into_any_element(),
- ListEntry::OutgoingRequest(user) => self
- .render_contact_request(user, false, is_selected, cx)
- .into_any_element(),
- ListEntry::Channel {
- channel,
- depth,
- has_children,
- } => self
- .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
- .into_any_element(),
- ListEntry::ChannelEditor { depth } => {
- self.render_channel_editor(*depth, cx).into_any_element()
- }
- ListEntry::ChannelInvite(channel) => self
- .render_channel_invite(channel, is_selected, cx)
- .into_any_element(),
- ListEntry::CallParticipant {
- user,
- peer_id,
- is_pending,
- } => self
- .render_call_participant(user, *peer_id, *is_pending, is_selected, cx)
- .into_any_element(),
- ListEntry::ParticipantProject {
- project_id,
- worktree_root_names,
- host_user_id,
- is_last,
- } => self
- .render_participant_project(
- *project_id,
- &worktree_root_names,
- *host_user_id,
- *is_last,
- is_selected,
- cx,
- )
- .into_any_element(),
- ListEntry::ParticipantScreen { peer_id, is_last } => self
- .render_participant_screen(*peer_id, *is_last, is_selected, cx)
- .into_any_element(),
- ListEntry::ChannelNotes { channel_id } => self
- .render_channel_notes(*channel_id, is_selected, cx)
- .into_any_element(),
- ListEntry::ChannelChat { channel_id } => self
- .render_channel_chat(*channel_id, is_selected, cx)
- .into_any_element(),
- }
- }
-
- fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
- v_stack()
- .size_full()
- .child(list(self.list_state.clone()).full())
- .child(
- v_stack().p_2().child(
- v_stack()
- .border_primary(cx)
- .border_t()
- .child(self.filter_editor.clone()),
- ),
- )
- }
-
- fn render_header(
- &self,
- section: Section,
- is_selected: bool,
- is_collapsed: bool,
- cx: &ViewContext<Self>,
- ) -> impl IntoElement {
- let mut channel_link = None;
- let mut channel_tooltip_text = None;
- let mut channel_icon = None;
- // let mut is_dragged_over = false;
-
- let text = match section {
- Section::ActiveCall => {
- let channel_name = maybe!({
- let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
-
- let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
-
- channel_link = Some(channel.link());
- (channel_icon, channel_tooltip_text) = match channel.visibility {
- proto::ChannelVisibility::Public => {
- (Some("icons/public.svg"), Some("Copy public channel link."))
- }
- proto::ChannelVisibility::Members => {
- (Some("icons/hash.svg"), Some("Copy private channel link."))
- }
- };
-
- Some(channel.name.as_ref())
- });
-
- if let Some(name) = channel_name {
- SharedString::from(format!("{}", name))
- } else {
- SharedString::from("Current Call")
- }
- }
- Section::ContactRequests => SharedString::from("Requests"),
- Section::Contacts => SharedString::from("Contacts"),
- Section::Channels => SharedString::from("Channels"),
- Section::ChannelInvites => SharedString::from("Invites"),
- Section::Online => SharedString::from("Online"),
- Section::Offline => SharedString::from("Offline"),
- };
-
- let button = match section {
- Section::ActiveCall => channel_link.map(|channel_link| {
- let channel_link_copy = channel_link.clone();
- IconButton::new("channel-link", Icon::Copy)
- .icon_size(IconSize::Small)
- .size(ButtonSize::None)
- .visible_on_hover("section-header")
- .on_click(move |_, cx| {
- let item = ClipboardItem::new(channel_link_copy.clone());
- cx.write_to_clipboard(item)
- })
- .tooltip(|cx| Tooltip::text("Copy channel link", cx))
- .into_any_element()
- }),
- Section::Contacts => Some(
- IconButton::new("add-contact", Icon::Plus)
- .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
- .tooltip(|cx| Tooltip::text("Search for new contact", cx))
- .into_any_element(),
- ),
- Section::Channels => Some(
- IconButton::new("add-channel", Icon::Plus)
- .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
- .tooltip(|cx| Tooltip::text("Create a channel", cx))
- .into_any_element(),
- ),
- _ => None,
- };
-
- let can_collapse = match section {
- Section::ActiveCall | Section::Channels | Section::Contacts => false,
- Section::ChannelInvites
- | Section::ContactRequests
- | Section::Online
- | Section::Offline => true,
- };
-
- h_stack()
- .w_full()
- .group("section-header")
- .child(
- ListHeader::new(text)
- .when(can_collapse, |header| {
- header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
- move |this, _, cx| {
- this.toggle_section_expanded(section, cx);
- },
- ))
- })
- .inset(true)
- .end_slot::<AnyElement>(button)
- .selected(is_selected),
- )
- .when(section == Section::Channels, |el| {
- el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
- .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(dragged_channel.id, None, cx)
- })
- .detach_and_log_err(cx)
- }))
- })
- }
-
- fn render_contact(
- &self,
- contact: &Contact,
- calling: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- let online = contact.online;
- let busy = contact.busy || calling;
- let user_id = contact.user.id;
- let github_login = SharedString::from(contact.user.github_login.clone());
- let item =
- ListItem::new(github_login.clone())
- .indent_level(1)
- .indent_step_size(px(20.))
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- .child(
- h_stack()
- .w_full()
- .justify_between()
- .child(Label::new(github_login.clone()))
- .when(calling, |el| {
- el.child(Label::new("Calling").color(Color::Muted))
- })
- .when(!calling, |el| {
- el.child(
- IconButton::new("remove_contact", Icon::Close)
- .icon_color(Color::Muted)
- .visible_on_hover("")
- .tooltip(|cx| Tooltip::text("Remove Contact", cx))
- .on_click(cx.listener({
- let github_login = github_login.clone();
- move |this, _, cx| {
- this.remove_contact(user_id, &github_login, cx);
- }
- })),
- )
- }),
- )
- .start_slot(
- // todo!() handle contacts with no avatar
- Avatar::new(contact.user.avatar_uri.clone())
- .availability_indicator(if online { Some(!busy) } else { None }),
- )
- .when(online && !busy, |el| {
- el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- });
-
- div()
- .id(github_login.clone())
- .group("")
- .child(item)
- .tooltip(move |cx| {
- let text = if !online {
- format!(" {} is offline", &github_login)
- } else if busy {
- format!(" {} is on a call", &github_login)
- } else {
- let room = ActiveCall::global(cx).read(cx).room();
- if room.is_some() {
- format!("Invite {} to join call", &github_login)
- } else {
- format!("Call {}", &github_login)
- }
- };
- Tooltip::text(text, cx)
- })
- }
-
- fn render_contact_request(
- &self,
- user: &Arc<User>,
- is_incoming: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- let github_login = SharedString::from(user.github_login.clone());
- let user_id = user.id;
- let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
- let color = if is_response_pending {
- Color::Muted
- } else {
- Color::Default
- };
-
- let controls = if is_incoming {
- vec![
- IconButton::new("decline-contact", Icon::Close)
- .on_click(cx.listener(move |this, _, cx| {
- this.respond_to_contact_request(user_id, false, cx);
- }))
- .icon_color(color)
- .tooltip(|cx| Tooltip::text("Decline invite", cx)),
- IconButton::new("accept-contact", Icon::Check)
- .on_click(cx.listener(move |this, _, cx| {
- this.respond_to_contact_request(user_id, true, cx);
- }))
- .icon_color(color)
- .tooltip(|cx| Tooltip::text("Accept invite", cx)),
- ]
- } else {
- let github_login = github_login.clone();
- vec![IconButton::new("remove_contact", Icon::Close)
- .on_click(cx.listener(move |this, _, cx| {
- this.remove_contact(user_id, &github_login, cx);
- }))
- .icon_color(color)
- .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
- };
-
- ListItem::new(github_login.clone())
- .indent_level(1)
- .indent_step_size(px(20.))
- .selected(is_selected)
- .child(
- h_stack()
- .w_full()
- .justify_between()
- .child(Label::new(github_login.clone()))
- .child(h_stack().children(controls)),
- )
- .start_slot(Avatar::new(user.avatar_uri.clone()))
- }
-
- fn render_channel_invite(
- &self,
- channel: &Arc<Channel>,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> ListItem {
- let channel_id = channel.id;
- let response_is_pending = self
- .channel_store
- .read(cx)
- .has_pending_channel_invite_response(&channel);
- let color = if response_is_pending {
- Color::Muted
- } else {
- Color::Default
- };
-
- let controls = [
- IconButton::new("reject-invite", Icon::Close)
- .on_click(cx.listener(move |this, _, cx| {
- this.respond_to_channel_invite(channel_id, false, cx);
- }))
- .icon_color(color)
- .tooltip(|cx| Tooltip::text("Decline invite", cx)),
- IconButton::new("accept-invite", Icon::Check)
- .on_click(cx.listener(move |this, _, cx| {
- this.respond_to_channel_invite(channel_id, true, cx);
- }))
- .icon_color(color)
- .tooltip(|cx| Tooltip::text("Accept invite", cx)),
- ];
-
- ListItem::new(("channel-invite", channel.id as usize))
- .selected(is_selected)
- .child(
- h_stack()
- .w_full()
- .justify_between()
- .child(Label::new(channel.name.clone()))
- .child(h_stack().children(controls)),
- )
- .start_slot(
- IconElement::new(Icon::Hash)
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- }
-
- fn render_contact_placeholder(
- &self,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> ListItem {
- ListItem::new("contact-placeholder")
- .child(IconElement::new(Icon::Plus))
- .child(Label::new("Add a Contact"))
- .selected(is_selected)
- .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
- }
-
- fn render_channel(
- &self,
- channel: &Channel,
- depth: usize,
- has_children: bool,
- is_selected: bool,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> impl IntoElement {
- let channel_id = channel.id;
-
- let is_active = maybe!({
- let call_channel = ActiveCall::global(cx)
- .read(cx)
- .room()?
- .read(cx)
- .channel_id()?;
- Some(call_channel == channel_id)
- })
- .unwrap_or(false);
- let is_public = self
- .channel_store
- .read(cx)
- .channel_for_id(channel_id)
- .map(|channel| channel.visibility)
- == Some(proto::ChannelVisibility::Public);
- let disclosed =
- has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
-
- let has_messages_notification = channel.unseen_message_id.is_some();
- let has_notes_notification = channel.unseen_note_version.is_some();
-
- const FACEPILE_LIMIT: usize = 3;
- let participants = self.channel_store.read(cx).channel_participants(channel_id);
-
- let face_pile = if !participants.is_empty() {
- let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
- let result = FacePile {
- faces: participants
- .iter()
- .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
- .take(FACEPILE_LIMIT)
- .chain(if extra_count > 0 {
- // todo!() @nate - this label looks wrong.
- Some(Label::new(format!("+{}", extra_count)).into_any_element())
- } else {
- None
- })
- .collect::<SmallVec<_>>(),
- };
-
- Some(result)
- } else {
- None
- };
-
- let button_container = |cx: &mut ViewContext<Self>| {
- h_stack()
- .absolute()
- // We're using a negative coordinate for the right anchor to
- // counteract the padding of the `ListItem`.
- //
- // This prevents a gap from showing up between the background
- // of this element and the edge of the collab panel.
- .right(rems(-0.5))
- // HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
- .z_index(10)
- .bg(cx.theme().colors().panel_background)
- .when(is_selected || is_active, |this| {
- this.bg(cx.theme().colors().ghost_element_selected)
- })
- };
-
- let messages_button = |cx: &mut ViewContext<Self>| {
- IconButton::new("channel_chat", Icon::MessageBubbles)
- .icon_size(IconSize::Small)
- .icon_color(if has_messages_notification {
- Color::Default
- } else {
- Color::Muted
- })
- .on_click(cx.listener(move |this, _, cx| this.join_channel_chat(channel_id, cx)))
- .tooltip(|cx| Tooltip::text("Open channel chat", cx))
- };
-
- let notes_button = |cx: &mut ViewContext<Self>| {
- IconButton::new("channel_notes", Icon::File)
- .icon_size(IconSize::Small)
- .icon_color(if has_notes_notification {
- Color::Default
- } else {
- Color::Muted
- })
- .on_click(cx.listener(move |this, _, cx| this.open_channel_notes(channel_id, cx)))
- .tooltip(|cx| Tooltip::text("Open channel notes", cx))
- };
-
- let width = self.width.unwrap_or(px(240.));
-
- div()
- .id(channel_id as usize)
- .group("")
- .flex()
- .w_full()
- .on_drag(channel.clone(), move |channel, cx| {
- cx.new_view(|_| DraggedChannelView {
- channel: channel.clone(),
- width,
- })
- })
- .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
- .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
- })
- .detach_and_log_err(cx)
- }))
- .child(
- ListItem::new(channel_id as usize)
- // Add one level of depth for the disclosure arrow.
- .indent_level(depth + 1)
- .indent_step_size(px(20.))
- .selected(is_selected || is_active)
- .toggle(disclosed)
- .on_toggle(
- cx.listener(move |this, _, cx| {
- this.toggle_channel_collapsed(channel_id, cx)
- }),
- )
- .on_click(cx.listener(move |this, _, cx| {
- if is_active {
- this.open_channel_notes(channel_id, cx)
- } else {
- this.join_channel(channel_id, cx)
- }
- }))
- .on_secondary_mouse_down(cx.listener(
- move |this, event: &MouseDownEvent, cx| {
- this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
- },
- ))
- .start_slot(
- IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(
- h_stack()
- .id(channel_id as usize)
- // HACK: This is a dirty hack to help with the positioning of the button container.
- //
- // We're using a pixel width for the elements but then allowing the contents to
- // overflow. This means that the label and facepile will be shown, but will not
- // push the button container off the edge of the panel.
- .w_px()
- .child(Label::new(channel.name.clone()))
- .children(face_pile.map(|face_pile| face_pile.render(cx))),
- )
- .end_slot::<Div>(
- // If we have a notification for either button, we want to show the corresponding
- // button(s) as indicators.
- if has_messages_notification || has_notes_notification {
- Some(
- button_container(cx).child(
- h_stack()
- .px_1()
- .children(
- // We only want to render the messages button if there are unseen messages.
- // This way we don't take up any space that might overlap the channel name
- // when there are no notifications.
- has_messages_notification.then(|| messages_button(cx)),
- )
- .child(
- // We always want the notes button to take up space to prevent layout
- // shift when hovering over the channel.
- // However, if there are is no notes notification we just show an empty slot.
- notes_button(cx)
- .when(!has_notes_notification, |this| {
- this.visible_on_hover("")
- }),
- ),
- ),
- )
- } else {
- None
- },
- )
- .end_hover_slot(
- // When we hover the channel entry we want to always show both buttons.
- button_container(cx).child(
- h_stack()
- .px_1()
- // The element hover background has a slight transparency to it, so we
- // need to apply it to the inner element so that it blends with the solid
- // background color of the absolutely-positioned element.
- .group_hover("", |style| {
- style.bg(cx.theme().colors().ghost_element_hover)
- })
- .child(messages_button(cx))
- .child(notes_button(cx)),
- ),
- ),
- )
- .tooltip(|cx| Tooltip::text("Join channel", cx))
- }
-
- fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- let item = ListItem::new("channel-editor")
- .inset(false)
- // Add one level of depth for the disclosure arrow.
- .indent_level(depth + 1)
- .indent_step_size(px(20.))
- .start_slot(
- IconElement::new(Icon::Hash)
- .size(IconSize::Small)
- .color(Color::Muted),
- );
-
- if let Some(pending_name) = self
- .channel_editing_state
- .as_ref()
- .and_then(|state| state.pending_name())
- {
- item.child(Label::new(pending_name))
- } else {
- item.child(
- div()
- .w_full()
- .py_1() // todo!() @nate this is a px off at the default font size.
- .child(self.channel_name_editor.clone()),
- )
- }
- }
-}
-
-fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
- let rem_size = cx.rem_size();
- let line_height = cx.text_style().line_height_in_pixels(rem_size);
- let width = rem_size * 1.5;
- let thickness = px(2.);
- let color = cx.theme().colors().text;
-
- canvas(move |bounds, cx| {
- let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
- let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
- let right = bounds.right();
- let top = bounds.top();
-
- cx.paint_quad(fill(
- Bounds::from_corners(
- point(start_x, top),
- point(
- start_x + thickness,
- if is_last { start_y } else { bounds.bottom() },
- ),
- ),
- color,
- ));
- cx.paint_quad(fill(
- Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
- color,
- ));
- })
- .w(width)
- .h(line_height)
-}
-
-impl Render for CollabPanel {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .key_context("CollabPanel")
- .on_action(cx.listener(CollabPanel::cancel))
- .on_action(cx.listener(CollabPanel::select_next))
- .on_action(cx.listener(CollabPanel::select_prev))
- .on_action(cx.listener(CollabPanel::confirm))
- .on_action(cx.listener(CollabPanel::insert_space))
- .on_action(cx.listener(CollabPanel::remove_selected_channel))
- .on_action(cx.listener(CollabPanel::show_inline_context_menu))
- .on_action(cx.listener(CollabPanel::rename_selected_channel))
- .on_action(cx.listener(CollabPanel::collapse_selected_channel))
- .on_action(cx.listener(CollabPanel::expand_selected_channel))
- .on_action(cx.listener(CollabPanel::start_move_selected_channel))
- .track_focus(&self.focus_handle)
- .size_full()
- .child(if self.user_store.read(cx).current_user().is_none() {
- self.render_signed_out(cx)
- } else {
- self.render_signed_in(cx)
- })
- .children(self.context_menu.as_ref().map(|(menu, position, _)| {
- overlay()
- .position(*position)
- .anchor(gpui::AnchorCorner::TopLeft)
- .child(menu.clone())
- }))
- }
-}
-
-impl EventEmitter<PanelEvent> for CollabPanel {}
-
-impl Panel for CollabPanel {
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- CollaborationPanelSettings::get_global(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::<CollaborationPanelSettings>(
- self.fs.clone(),
- cx,
- move |settings| settings.dock = Some(position),
- );
- }
-
- fn size(&self, cx: &gpui::WindowContext) -> Pixels {
- self.width
- .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
- }
-
- fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
- CollaborationPanelSettings::get_global(cx)
- .button
- .then(|| ui::Icon::Collab)
- }
-
- fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
- Some("Collab Panel")
- }
-
- fn toggle_action(&self) -> Box<dyn gpui::Action> {
- Box::new(ToggleFocus)
- }
-
- fn persistent_name() -> &'static str {
- "CollabPanel"
- }
-}
-
-impl FocusableView for CollabPanel {
- fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
- self.filter_editor.focus_handle(cx).clone()
- }
-}
-
-impl PartialEq for ListEntry {
- fn eq(&self, other: &Self) -> bool {
- match self {
- ListEntry::Header(section_1) => {
- if let ListEntry::Header(section_2) = other {
- return section_1 == section_2;
- }
- }
- ListEntry::CallParticipant { user: user_1, .. } => {
- if let ListEntry::CallParticipant { user: user_2, .. } = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::ParticipantProject {
- project_id: project_id_1,
- ..
- } => {
- if let ListEntry::ParticipantProject {
- project_id: project_id_2,
- ..
- } = other
- {
- return project_id_1 == project_id_2;
- }
- }
- ListEntry::ParticipantScreen {
- peer_id: peer_id_1, ..
- } => {
- if let ListEntry::ParticipantScreen {
- peer_id: peer_id_2, ..
- } = other
- {
- return peer_id_1 == peer_id_2;
- }
- }
- ListEntry::Channel {
- channel: channel_1, ..
- } => {
- if let ListEntry::Channel {
- channel: channel_2, ..
- } = other
- {
- return channel_1.id == channel_2.id;
- }
- }
- ListEntry::ChannelNotes { channel_id } => {
- if let ListEntry::ChannelNotes {
- channel_id: other_id,
- } = other
- {
- return channel_id == other_id;
- }
- }
- ListEntry::ChannelChat { channel_id } => {
- if let ListEntry::ChannelChat {
- channel_id: other_id,
- } = other
- {
- return channel_id == other_id;
- }
- }
- ListEntry::ChannelInvite(channel_1) => {
- if let ListEntry::ChannelInvite(channel_2) = other {
- return channel_1.id == channel_2.id;
- }
- }
- ListEntry::IncomingRequest(user_1) => {
- if let ListEntry::IncomingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::OutgoingRequest(user_1) => {
- if let ListEntry::OutgoingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::Contact {
- contact: contact_1, ..
- } => {
- if let ListEntry::Contact {
- contact: contact_2, ..
- } = other
- {
- return contact_1.user.id == contact_2.user.id;
- }
- }
- ListEntry::ChannelEditor { depth } => {
- if let ListEntry::ChannelEditor { depth: other_depth } = other {
- return depth == other_depth;
- }
- }
- ListEntry::ContactPlaceholder => {
- if let ListEntry::ContactPlaceholder = other {
- return true;
- }
- }
- }
- false
- }
-}
-
-struct DraggedChannelView {
- channel: Channel,
- width: Pixels,
-}
-
-impl Render for DraggedChannelView {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
- let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
- h_stack()
- .font(ui_font)
- .bg(cx.theme().colors().background)
- .w(self.width)
- .p_1()
- .gap_1()
- .child(
- IconElement::new(
- if self.channel.visibility == proto::ChannelVisibility::Public {
- Icon::Public
- } else {
- Icon::Hash
- },
- )
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .child(Label::new(self.channel.name.clone()))
- }
-}
@@ -1,575 +0,0 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore};
-use client::{
- proto::{self, ChannelRole, ChannelVisibility},
- User, UserId, UserStore,
-};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
- actions, div, overlay, AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusableView,
- Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext,
- WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use std::sync::Arc;
-use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
-use util::TryFutureExt;
-use workspace::ModalView;
-
-actions!(
- channel_modal,
- [
- SelectNextControl,
- ToggleMode,
- ToggleMemberAdmin,
- RemoveMember
- ]
-);
-
-pub struct ChannelModal {
- picker: View<Picker<ChannelModalDelegate>>,
- channel_store: Model<ChannelStore>,
- channel_id: ChannelId,
-}
-
-impl ChannelModal {
- pub fn new(
- user_store: Model<UserStore>,
- channel_store: Model<ChannelStore>,
- channel_id: ChannelId,
- mode: Mode,
- members: Vec<ChannelMembership>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
- let channel_modal = cx.view().downgrade();
- let picker = cx.new_view(|cx| {
- Picker::new(
- ChannelModalDelegate {
- channel_modal,
- 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(),
- context_menu: None,
- members,
- mode,
- },
- cx,
- )
- .modal(false)
- });
-
- Self {
- picker,
- channel_store,
- channel_id,
- }
- }
-
- 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.members = members);
- })?;
- }
-
- this.update(&mut cx, |this, cx| {
- this.picker.update(cx, |picker, cx| {
- let delegate = &mut picker.delegate;
- delegate.mode = mode;
- delegate.selected_index = 0;
- picker.set_query("", cx);
- picker.update_matches(picker.query(cx), cx);
- cx.notify()
- });
- cx.notify()
- })
- })
- .detach();
- }
-
- fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
- self.channel_store.update(cx, |channel_store, cx| {
- channel_store
- .set_channel_visibility(
- self.channel_id,
- match selection {
- Selection::Unselected => ChannelVisibility::Members,
- Selection::Selected => ChannelVisibility::Public,
- Selection::Indeterminate => return,
- },
- cx,
- )
- .detach_and_log_err(cx)
- });
- }
-
- fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(DismissEvent);
- }
-}
-
-impl EventEmitter<DismissEvent> for ChannelModal {}
-impl ModalView for ChannelModal {}
-
-impl FocusableView for ChannelModal {
- fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for ChannelModal {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let channel_store = self.channel_store.read(cx);
- let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
- return div();
- };
- let channel_name = channel.name.clone();
- let channel_id = channel.id;
- let visibility = channel.visibility;
- let mode = self.picker.read(cx).delegate.mode;
-
- v_stack()
- .key_context("ChannelModal")
- .on_action(cx.listener(Self::toggle_mode))
- .on_action(cx.listener(Self::dismiss))
- .elevation_3(cx)
- .w(rems(34.))
- .child(
- v_stack()
- .px_2()
- .py_1()
- .rounded_t(px(8.))
- .bg(cx.theme().colors().element_background)
- .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
- .child(Label::new(channel_name))
- .child(
- h_stack()
- .w_full()
- .justify_between()
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "is-public",
- if visibility == ChannelVisibility::Public {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(cx.listener(Self::set_channel_visiblity)),
- )
- .child(Label::new("Public")),
- )
- .children(if visibility == ChannelVisibility::Public {
- Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
- 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);
- }
- },
- )))
- } else {
- None
- }),
- )
- .child(
- div()
- .w_full()
- .flex()
- .flex_row()
- .child(
- Button::new("manage-members", "Manage Members")
- .selected(mode == Mode::ManageMembers)
- .on_click(cx.listener(|this, _, cx| {
- this.set_mode(Mode::ManageMembers, cx);
- })),
- )
- .child(
- Button::new("invite-members", "Invite Members")
- .selected(mode == Mode::InviteMembers)
- .on_click(cx.listener(|this, _, cx| {
- this.set_mode(Mode::InviteMembers, cx);
- })),
- ),
- ),
- )
- .child(self.picker.clone())
- }
-}
-
-#[derive(Copy, Clone, PartialEq)]
-pub enum Mode {
- ManageMembers,
- InviteMembers,
-}
-
-pub struct ChannelModalDelegate {
- channel_modal: WeakView<ChannelModal>,
- matching_users: Vec<Arc<User>>,
- matching_member_indices: Vec<usize>,
- user_store: Model<UserStore>,
- channel_store: Model<ChannelStore>,
- channel_id: ChannelId,
- selected_index: usize,
- mode: Mode,
- match_candidates: Vec<StringMatchCandidate>,
- members: Vec<ChannelMembership>,
- context_menu: Option<(View<ContextMenu>, Subscription)>,
-}
-
-impl PickerDelegate for ChannelModalDelegate {
- type ListItem = ListItem;
-
- 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_executor().block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- cx.background_executor().clone(),
- ));
-
- cx.spawn(|picker, mut cx| async move {
- picker
- .update(&mut cx, |picker, cx| {
- let delegate = &mut picker.delegate;
- 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| {
- picker.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(selected_user, role.unwrap_or(ChannelRole::Member), cx)
- }
- Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
- Some(proto::channel_member::Kind::Invitee) => {
- self.remove_member(selected_user.id, 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>>) {
- if self.context_menu.is_none() {
- self.channel_modal
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let (user, role) = self.user_at_index(ix)?;
- let request_status = self.member_status(user.id, cx);
-
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .selected(selected)
- .start_slot(Avatar::new(user.avatar_uri.clone()))
- .child(Label::new(user.github_login.clone()))
- .end_slot(h_stack().gap_2().map(|slot| {
- match self.mode {
- Mode::ManageMembers => slot
- .children(
- if request_status == Some(proto::channel_member::Kind::Invitee) {
- Some(Label::new("Invited"))
- } else {
- None
- },
- )
- .children(match role {
- Some(ChannelRole::Admin) => Some(Label::new("Admin")),
- Some(ChannelRole::Guest) => Some(Label::new("Guest")),
- _ => None,
- })
- .child(IconButton::new("ellipsis", Icon::Ellipsis))
- .children(
- if let (Some((menu, _)), true) = (&self.context_menu, selected) {
- Some(
- overlay()
- .anchor(gpui::AnchorCorner::TopLeft)
- .child(menu.clone()),
- )
- } else {
- None
- },
- ),
- Mode::InviteMembers => match request_status {
- Some(proto::channel_member::Kind::Invitee) => {
- slot.children(Some(Label::new("Invited")))
- }
- Some(proto::channel_member::Kind::Member) => {
- slot.children(Some(Label::new("Member")))
- }
- _ => slot,
- },
- }
- })),
- )
- }
-}
-
-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 set_user_role(
- &mut self,
- user_id: UserId,
- new_role: ChannelRole,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<()> {
- 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 = &mut picker.delegate;
- 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_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
- 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 = &mut picker.delegate;
- 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));
-
- picker.focus(cx);
- 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.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,
- user: Arc<User>,
- role: ChannelRole,
- cx: &mut ViewContext<Picker<Self>>,
- ) {
- let user_id = user.id;
- let picker = cx.view().clone();
- let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
- menu = menu.entry("Remove Member", None, {
- let picker = picker.clone();
- move |cx| {
- picker.update(cx, |picker, cx| {
- picker.delegate.remove_member(user_id, cx);
- })
- }
- });
-
- let picker = picker.clone();
- match role {
- ChannelRole::Admin => {
- menu = menu.entry("Revoke Admin", None, move |cx| {
- picker.update(cx, |picker, cx| {
- picker
- .delegate
- .set_user_role(user_id, ChannelRole::Member, cx);
- })
- });
- }
- ChannelRole::Member => {
- menu = menu.entry("Make Admin", None, move |cx| {
- picker.update(cx, |picker, cx| {
- picker
- .delegate
- .set_user_role(user_id, ChannelRole::Admin, cx);
- })
- });
- }
- _ => {}
- };
-
- menu
- });
- cx.focus_view(&context_menu);
- let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
- picker.delegate.context_menu = None;
- picker.focus(cx);
- cx.notify();
- });
- self.context_menu = Some((context_menu, subscription));
- }
-}
@@ -1,163 +0,0 @@
-use client::{ContactRequestStatus, User, UserStore};
-use gpui::{
- AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _,
- Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use std::sync::Arc;
-use theme::ActiveTheme as _;
-use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
-use util::{ResultExt as _, TryFutureExt};
-use workspace::ModalView;
-
-pub struct ContactFinder {
- picker: View<Picker<ContactFinderDelegate>>,
-}
-
-impl ContactFinder {
- pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
- let delegate = ContactFinderDelegate {
- parent: cx.view().downgrade(),
- user_store,
- potential_contacts: Arc::from([]),
- selected_index: 0,
- };
- let picker = cx.new_view(|cx| Picker::new(delegate, cx).modal(false));
-
- Self { picker }
- }
-
- pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
- self.picker.update(cx, |picker, cx| {
- picker.set_query(query, cx);
- });
- }
-}
-
-impl Render for ContactFinder {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .elevation_3(cx)
- .child(
- v_stack()
- .px_2()
- .py_1()
- .bg(cx.theme().colors().element_background)
- // HACK: Prevent the background color from overflowing the parent container.
- .rounded_t(px(8.))
- .child(Label::new("Contacts"))
- .child(h_stack().child(Label::new("Invite new contacts"))),
- )
- .child(self.picker.clone())
- .w(rems(34.))
- }
-}
-
-pub struct ContactFinderDelegate {
- parent: WeakView<ContactFinder>,
- potential_contacts: Arc<[Arc<User>]>,
- user_store: Model<UserStore>,
- selected_index: usize,
-}
-
-impl EventEmitter<DismissEvent> for ContactFinder {}
-impl ModalView for ContactFinder {}
-
-impl FocusableView for ContactFinder {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl PickerDelegate for ContactFinderDelegate {
- type ListItem = ListItem;
-
- 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 placeholder_text(&self) -> Arc<str> {
- "Search collaborator by username...".into()
- }
-
- 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.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>>) {
- self.parent
- .update(cx, |_, cx| cx.emit(DismissEvent))
- .log_err();
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- 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,
- };
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .selected(selected)
- .start_slot(Avatar::new(user.avatar_uri.clone()))
- .child(Label::new(user.github_login.clone()))
- .end_slot::<IconElement>(
- icon_path.map(|icon_path| IconElement::from_path(icon_path)),
- ),
- )
- }
-}
@@ -1,586 +0,0 @@
-use crate::face_pile::FacePile;
-use auto_update::AutoUpdateStatus;
-use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
-use gpui::{
- actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
- InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
- StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
- WindowBounds,
-};
-use project::{Project, RepositoryEntry};
-use recent_projects::RecentProjects;
-use std::sync::Arc;
-use theme::{ActiveTheme, PlayerColors};
-use ui::{
- h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
- IconButton, IconElement, Tooltip,
-};
-use util::ResultExt;
-use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{notifications::NotifyResultExt, Workspace};
-
-const MAX_PROJECT_NAME_LENGTH: usize = 40;
-const MAX_BRANCH_NAME_LENGTH: usize = 40;
-
-actions!(
- collab,
- [
- ShareProject,
- UnshareProject,
- ToggleUserMenu,
- ToggleProjectMenu,
- SwitchBranch
- ]
-);
-
-pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|workspace: &mut Workspace, cx| {
- let titlebar_item = cx.new_view(|cx| CollabTitlebarItem::new(workspace, cx));
- workspace.set_titlebar_item(titlebar_item.into(), cx)
- })
- .detach();
- // cx.add_action(CollabTitlebarItem::share_project);
- // cx.add_action(CollabTitlebarItem::unshare_project);
- // cx.add_action(CollabTitlebarItem::toggle_user_menu);
- // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
- // cx.add_action(CollabTitlebarItem::toggle_project_menu);
-}
-
-pub struct CollabTitlebarItem {
- project: Model<Project>,
- user_store: Model<UserStore>,
- client: Arc<Client>,
- workspace: WeakView<Workspace>,
- _subscriptions: Vec<Subscription>,
-}
-
-impl Render for CollabTitlebarItem {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let room = ActiveCall::global(cx).read(cx).room().cloned();
- let current_user = self.user_store.read(cx).current_user();
- let client = self.client.clone();
- let project_id = self.project.read(cx).remote_id();
-
- h_stack()
- .id("titlebar")
- .justify_between()
- .w_full()
- .h(rems(1.75))
- // Set a non-scaling min-height here to ensure the titlebar is
- // always at least the height of the traffic lights.
- .min_h(px(32.))
- .map(|this| {
- if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
- this.pl_2()
- } else {
- // Use pixels here instead of a rem-based size because the macOS traffic
- // lights are a static size, and don't scale with the rest of the UI.
- this.pl(px(80.))
- }
- })
- .bg(cx.theme().colors().title_bar_background)
- .on_click(|event, cx| {
- if event.up.click_count == 2 {
- cx.zoom_window();
- }
- })
- // left side
- .child(
- h_stack()
- .gap_1()
- .children(self.render_project_host(cx))
- .child(self.render_project_name(cx))
- .children(self.render_project_branch(cx))
- .when_some(
- current_user.clone().zip(client.peer_id()).zip(room.clone()),
- |this, ((current_user, peer_id), room)| {
- let player_colors = cx.theme().players();
- let room = room.read(cx);
- let mut remote_participants =
- room.remote_participants().values().collect::<Vec<_>>();
- remote_participants.sort_by_key(|p| p.participant_index.0);
-
- this.children(self.render_collaborator(
- ¤t_user,
- peer_id,
- true,
- room.is_speaking(),
- room.is_muted(cx),
- &room,
- project_id,
- ¤t_user,
- ))
- .children(
- remote_participants.iter().filter_map(|collaborator| {
- let is_present = project_id.map_or(false, |project_id| {
- collaborator.location
- == ParticipantLocation::SharedProject { project_id }
- });
-
- let face_pile = self.render_collaborator(
- &collaborator.user,
- collaborator.peer_id,
- is_present,
- collaborator.speaking,
- collaborator.muted,
- &room,
- project_id,
- ¤t_user,
- )?;
-
- Some(
- v_stack()
- .id(("collaborator", collaborator.user.id))
- .child(face_pile)
- .child(render_color_ribbon(
- collaborator.participant_index,
- player_colors,
- ))
- .cursor_pointer()
- .on_click({
- let peer_id = collaborator.peer_id;
- cx.listener(move |this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- workspace.follow(peer_id, cx);
- })
- .ok();
- })
- })
- .tooltip({
- let login = collaborator.user.github_login.clone();
- move |cx| {
- Tooltip::text(format!("Follow {login}"), cx)
- }
- }),
- )
- }),
- )
- },
- ),
- )
- // right side
- .child(
- h_stack()
- .gap_1()
- .pr_1()
- .when_some(room, |this, room| {
- let room = room.read(cx);
- let project = self.project.read(cx);
- let is_local = project.is_local();
- let is_shared = is_local && project.is_shared();
- let is_muted = room.is_muted(cx);
- let is_deafened = room.is_deafened().unwrap_or(false);
- let is_screen_sharing = room.is_screen_sharing();
-
- this.when(is_local, |this| {
- this.child(
- Button::new(
- "toggle_sharing",
- if is_shared { "Unshare" } else { "Share" },
- )
- .style(ButtonStyle::Subtle)
- .label_size(LabelSize::Small)
- .on_click(cx.listener(
- move |this, _, cx| {
- if is_shared {
- this.unshare_project(&Default::default(), cx);
- } else {
- this.share_project(&Default::default(), cx);
- }
- },
- )),
- )
- })
- .child(
- IconButton::new("leave-call", ui::Icon::Exit)
- .style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .on_click(move |_, cx| {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- }),
- )
- .child(
- IconButton::new(
- "mute-microphone",
- if is_muted {
- ui::Icon::MicMute
- } else {
- ui::Icon::Mic
- },
- )
- .style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .selected(is_muted)
- .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
- )
- .child(
- IconButton::new(
- "mute-sound",
- if is_deafened {
- ui::Icon::AudioOff
- } else {
- ui::Icon::AudioOn
- },
- )
- .style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .selected(is_deafened)
- .tooltip(move |cx| {
- Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
- })
- .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
- )
- .child(
- IconButton::new("screen-share", ui::Icon::Screen)
- .style(ButtonStyle::Subtle)
- .icon_size(IconSize::Small)
- .selected(is_screen_sharing)
- .on_click(move |_, cx| {
- crate::toggle_screen_sharing(&Default::default(), cx)
- }),
- )
- })
- .map(|el| {
- let status = self.client.status();
- let status = &*status.borrow();
- if matches!(status, client::Status::Connected { .. }) {
- el.child(self.render_user_menu_button(cx))
- } else {
- el.children(self.render_connection_status(status, cx))
- .child(self.render_sign_in_button(cx))
- .child(self.render_user_menu_button(cx))
- }
- }),
- )
- }
-}
-
-fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
- let color = colors.color_for_participant(participant_index.0).cursor;
- canvas(move |bounds, cx| {
- let mut path = Path::new(bounds.lower_left());
- let height = bounds.size.height;
- path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
- path.line_to(bounds.upper_right() - point(height, px(0.)));
- path.curve_to(bounds.lower_right(), bounds.upper_right());
- path.line_to(bounds.lower_left());
- cx.paint_path(path, color);
- })
- .h_1()
- .w_full()
-}
-
-impl CollabTitlebarItem {
- pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
- let project = workspace.project().clone();
- let user_store = workspace.app_state().user_store.clone();
- let client = workspace.app_state().client.clone();
- let active_call = ActiveCall::global(cx);
- let mut subscriptions = Vec::new();
- subscriptions.push(
- cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
- cx.notify()
- }),
- );
- subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
- subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
- subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
- subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
-
- Self {
- workspace: workspace.weak_handle(),
- project,
- user_store,
- client,
- _subscriptions: subscriptions,
- }
- }
-
- // resolve if you are in a room -> render_project_owner
- // render_project_owner -> resolve if you are in a room -> Option<foo>
-
- pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
- let host = self.project.read(cx).host()?;
- let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
- let participant_index = self
- .user_store
- .read(cx)
- .participant_indices()
- .get(&host.id)?;
- Some(
- div().border().border_color(gpui::red()).child(
- Button::new("project_owner_trigger", host.github_login.clone())
- .color(Color::Player(participant_index.0))
- .style(ButtonStyle::Subtle)
- .label_size(LabelSize::Small)
- .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
- ),
- )
- }
-
- pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
- let name = {
- let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
- let worktree = worktree.read(cx);
- worktree.root_name()
- });
-
- names.next().unwrap_or("")
- };
-
- let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
- let workspace = self.workspace.clone();
- popover_menu("project_name_trigger")
- .trigger(
- Button::new("project_name_trigger", name)
- .style(ButtonStyle::Subtle)
- .label_size(LabelSize::Small)
- .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
- )
- .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
- }
-
- pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
- let entry = {
- let mut names_and_branches =
- self.project.read(cx).visible_worktrees(cx).map(|worktree| {
- let worktree = worktree.read(cx);
- worktree.root_git_entry()
- });
-
- names_and_branches.next().flatten()
- };
- let workspace = self.workspace.upgrade()?;
- let branch_name = entry
- .as_ref()
- .and_then(RepositoryEntry::branch)
- .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
- Some(
- popover_menu("project_branch_trigger")
- .trigger(
- Button::new("project_branch_trigger", branch_name)
- .color(Color::Muted)
- .style(ButtonStyle::Subtle)
- .label_size(LabelSize::Small)
- .tooltip(move |cx| {
- Tooltip::with_meta(
- "Recent Branches",
- Some(&ToggleVcsMenu),
- "Local branches only",
- cx,
- )
- }),
- )
- .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
- )
- }
-
- fn render_collaborator(
- &self,
- user: &Arc<User>,
- peer_id: PeerId,
- is_present: bool,
- is_speaking: bool,
- is_muted: bool,
- room: &Room,
- project_id: Option<u64>,
- current_user: &Arc<User>,
- ) -> Option<FacePile> {
- let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
-
- let pile = FacePile::default()
- .child(
- Avatar::new(user.avatar_uri.clone())
- .grayscale(!is_present)
- .border_color(if is_speaking {
- gpui::blue()
- } else if is_muted {
- gpui::red()
- } else {
- Hsla::default()
- }),
- )
- .children(followers.iter().filter_map(|follower_peer_id| {
- let follower = room
- .remote_participants()
- .values()
- .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
- .or_else(|| {
- (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
- })?
- .clone();
-
- Some(Avatar::new(follower.avatar_uri.clone()))
- }));
-
- Some(pile)
- }
-
- fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
- let project = if cx.is_window_active() {
- Some(self.project.clone())
- } else {
- None
- };
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
- .detach_and_log_err(cx);
- }
-
- fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
- cx.notify();
- }
-
- fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
- let active_call = ActiveCall::global(cx);
- let project = self.project.clone();
- active_call
- .update(cx, |call, cx| call.share_project(project, cx))
- .detach_and_log_err(cx);
- }
-
- fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
- let active_call = ActiveCall::global(cx);
- let project = self.project.clone();
- active_call
- .update(cx, |call, cx| call.unshare_project(project, cx))
- .log_err();
- }
-
- pub fn render_vcs_popover(
- workspace: View<Workspace>,
- cx: &mut WindowContext<'_>,
- ) -> Option<View<BranchList>> {
- let view = build_branch_list(workspace, cx).log_err()?;
- let focus_handle = view.focus_handle(cx);
- cx.focus(&focus_handle);
- Some(view)
- }
-
- pub fn render_project_popover(
- workspace: WeakView<Workspace>,
- cx: &mut WindowContext<'_>,
- ) -> View<RecentProjects> {
- let view = RecentProjects::open_popover(workspace, cx);
-
- let focus_handle = view.focus_handle(cx);
- cx.focus(&focus_handle);
- view
- }
-
- fn render_connection_status(
- &self,
- status: &client::Status,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement> {
- match status {
- client::Status::ConnectionError
- | client::Status::ConnectionLost
- | client::Status::Reauthenticating { .. }
- | client::Status::Reconnecting { .. }
- | client::Status::ReconnectionError { .. } => Some(
- div()
- .id("disconnected")
- .bg(gpui::red()) // todo!() @nate
- .child(IconElement::new(Icon::Disconnected))
- .tooltip(|cx| Tooltip::text("Disconnected", cx))
- .into_any_element(),
- ),
- client::Status::UpgradeRequired => {
- let auto_updater = auto_update::AutoUpdater::get(cx);
- let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
- Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
- Some(AutoUpdateStatus::Installing)
- | Some(AutoUpdateStatus::Downloading)
- | Some(AutoUpdateStatus::Checking) => "Updating...",
- Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
- "Please update Zed to Collaborate"
- }
- };
-
- Some(
- div()
- .bg(gpui::red()) // todo!() @nate
- .child(Button::new("connection-status", label).on_click(|_, cx| {
- if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
- if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
- workspace::restart(&Default::default(), cx);
- return;
- }
- }
- auto_update::check(&Default::default(), cx);
- }))
- .into_any_element(),
- )
- }
- _ => None,
- }
- }
-
- pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
- let client = self.client.clone();
- Button::new("sign_in", "Sign in").on_click(move |_, cx| {
- let client = client.clone();
- cx.spawn(move |mut cx| async move {
- client
- .authenticate_and_connect(true, &cx)
- .await
- .notify_async_err(&mut cx);
- })
- .detach();
- })
- }
-
- pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
- if let Some(user) = self.user_store.read(cx).current_user() {
- popover_menu("user-menu")
- .menu(|cx| {
- ContextMenu::build(cx, |menu, _| {
- menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
- .action("Theme", theme_selector::Toggle.boxed_clone())
- .separator()
- .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
- .action("Sign Out", client::SignOut.boxed_clone())
- })
- .into()
- })
- .trigger(
- ButtonLike::new("user-menu")
- .child(
- h_stack()
- .gap_0p5()
- .child(Avatar::new(user.avatar_uri.clone()))
- .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
- )
- .style(ButtonStyle::Subtle)
- .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
- )
- .anchor(gpui::AnchorCorner::TopRight)
- } else {
- popover_menu("user-menu")
- .menu(|cx| {
- ContextMenu::build(cx, |menu, _| {
- menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
- .action("Theme", theme_selector::Toggle.boxed_clone())
- .separator()
- .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
- })
- .into()
- })
- .trigger(
- ButtonLike::new("user-menu")
- .child(
- h_stack()
- .gap_0p5()
- .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
- )
- .style(ButtonStyle::Subtle)
- .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
- )
- }
- }
-}
@@ -1,167 +0,0 @@
-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::{rc::Rc, sync::Arc};
-
-use call::{report_call_event_for_room, ActiveCall, Room};
-pub use collab_panel::CollabPanel;
-pub use collab_titlebar_item::CollabTitlebarItem;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
-use gpui::{
- actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
- WindowKind, WindowOptions,
-};
-pub use panel_settings::{
- ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
-};
-use settings::Settings;
-use util::ResultExt;
-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);
- channel_view::init(cx);
- chat_panel::init(cx);
- notification_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 PlatformDisplay>,
- window_size: Size<Pixels>,
-) -> WindowOptions {
- let notification_margin_width = GlobalPixels::from(16.);
- let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
-
- let screen_bounds = screen.bounds();
- let size: Size<GlobalPixels> = window_size.into();
-
- // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
- let bounds = gpui::Bounds::<GlobalPixels> {
- origin: screen_bounds.upper_right()
- - point(
- size.width + notification_margin_width,
- notification_margin_height,
- ),
- size: window_size.into(),
- };
- WindowOptions {
- bounds: WindowBounds::Fixed(bounds),
- titlebar: None,
- center: false,
- focus: false,
- show: true,
- kind: WindowKind::PopUp,
- is_movable: false,
- display_id: Some(screen.id()),
- }
-}
-
-// 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>()
-}
@@ -1,30 +0,0 @@
-use gpui::{
- div, AnyElement, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
-};
-use smallvec::SmallVec;
-
-#[derive(Default, IntoElement)]
-pub struct FacePile {
- pub faces: SmallVec<[AnyElement; 2]>,
-}
-
-impl RenderOnce for FacePile {
- fn render(self, _: &mut WindowContext) -> impl IntoElement {
- let player_count = self.faces.len();
- let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
- let isnt_last = ix < player_count - 1;
-
- div()
- .z_index((player_count - ix) as u8)
- .when(isnt_last, |div| div.neg_mr_1())
- .child(player)
- });
- div().p_1().flex().items_center().children(player_list)
- }
-}
-
-impl ParentElement for FacePile {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
- &mut self.faces
- }
-}
@@ -1,755 +0,0 @@
-use crate::{chat_panel::ChatPanel, 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, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
- CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
- InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
- ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
- VisualContext, WeakView, WindowContext,
-};
-use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-use project::Fs;
-use rpc::proto;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use std::{sync::Arc, time::Duration};
-use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
- dock::{DockPosition, Panel, PanelEvent},
- 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: Model<UserStore>,
- channel_store: Model<ChannelStore>,
- notification_store: Model<NotificationStore>,
- fs: Arc<dyn Fs>,
- width: Option<Pixels>,
- active: bool,
- notification_list: ListState,
- pending_serialization: Task<Option<()>>,
- subscriptions: Vec<gpui::Subscription>,
- workspace: WeakView<Workspace>,
- current_notification_toast: Option<(u64, Task<()>)>,
- local_timezone: UtcOffset,
- focus_handle: FocusHandle,
- mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedNotificationPanel {
- width: Option<Pixels>,
-}
-
-#[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) {
- cx.observe_new_views(|workspace: &mut Workspace, _| {
- workspace.register_action(|workspace, _: &ToggleFocus, cx| {
- workspace.toggle_panel_focus::<NotificationPanel>(cx);
- });
- })
- .detach();
-}
-
-impl NotificationPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
- let fs = workspace.app_state().fs.clone();
- let client = workspace.app_state().client.clone();
- let user_store = workspace.app_state().user_store.clone();
- let workspace_handle = workspace.weak_handle();
-
- cx.new_view(|cx: &mut ViewContext<Self>| {
- 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 view = cx.view().downgrade();
- let notification_list =
- ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
- view.upgrade()
- .and_then(|view| {
- view.update(cx, |this, cx| this.render_notification(ix, cx))
- })
- .unwrap_or_else(|| div().into_any())
- });
- notification_list.set_scroll_handler(cx.listener(
- |this, event: &ListScrollEvent, cx| {
- if event.count.saturating_sub(event.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.local_timezone(),
- channel_store: ChannelStore::global(cx),
- notification_store: NotificationStore::global(cx),
- notification_list,
- pending_serialization: Task::ready(None),
- workspace: workspace_handle,
- focus_handle: cx.focus_handle(),
- 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: WeakView<Workspace>,
- cx: AsyncWindowContext,
- ) -> Task<Result<View<Self>>> {
- cx.spawn(|mut cx| async move {
- let serialized_panel = if let Some(panel) = cx
- .background_executor()
- .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_executor().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> {
- 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 response = entry.response;
- let notification = entry.notification.clone();
-
- if self.active && !entry.is_read {
- self.did_render_notification(notification_id, ¬ification, cx);
- }
-
- Some(
- div()
- .id(ix)
- .flex()
- .flex_row()
- .size_full()
- .px_2()
- .py_1()
- .gap_2()
- .when(can_navigate, |el| {
- el.cursor(CursorStyle::PointingHand).on_click({
- let notification = notification.clone();
- cx.listener(move |this, _, cx| {
- this.did_click_notification(¬ification, cx)
- })
- })
- })
- .children(actor.map(|actor| {
- img(actor.avatar_uri.clone())
- .flex_none()
- .w_8()
- .h_8()
- .rounded_full()
- }))
- .child(
- v_stack()
- .gap_1()
- .size_full()
- .overflow_hidden()
- .child(Label::new(text.clone()))
- .child(
- h_stack()
- .child(
- Label::new(format_timestamp(
- timestamp,
- now,
- self.local_timezone,
- ))
- .color(Color::Muted),
- )
- .children(if let Some(is_accepted) = response {
- Some(div().flex().flex_grow().justify_end().child(Label::new(
- if is_accepted {
- "You accepted"
- } else {
- "You declined"
- },
- )))
- } else if needs_response {
- Some(
- h_stack()
- .flex_grow()
- .justify_end()
- .child(Button::new("decline", "Decline").on_click({
- let notification = notification.clone();
- let view = cx.view().clone();
- move |_, cx| {
- view.update(cx, |this, cx| {
- this.respond_to_notification(
- notification.clone(),
- false,
- cx,
- )
- });
- }
- }))
- .child(Button::new("accept", "Accept").on_click({
- let notification = notification.clone();
- let view = cx.view().clone();
- move |_, cx| {
- view.update(cx, |this, cx| {
- this.respond_to_notification(
- notification.clone(),
- true,
- cx,
- )
- });
- }
- })),
- )
- } else {
- None
- }),
- ),
- )
- .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_executor().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.window_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: &ViewContext<Self>) -> bool {
- if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
- if let Some(workspace) = self.workspace.upgrade() {
- return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
- let panel = panel.read(cx);
- panel.is_scrolled_to_bottom()
- && panel
- .active_chat()
- .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
- } else {
- false
- };
- }
- }
-
- false
- }
-
- fn on_notification_event(
- &mut self,
- _: Model<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_executor().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.view().downgrade();
- cx.new_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 Render for NotificationPanel {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .size_full()
- .child(
- h_stack()
- .justify_between()
- .px_2()
- .py_1()
- // Match the height of the tab bar so they line up.
- .h(rems(ui::Tab::HEIGHT_IN_REMS))
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .child(Label::new("Notifications"))
- .child(IconElement::new(Icon::Envelope)),
- )
- .map(|this| {
- if self.client.user_id().is_none() {
- this.child(
- v_stack()
- .gap_2()
- .p_4()
- .child(
- Button::new("sign_in_prompt_button", "Sign in")
- .icon_color(Color::Muted)
- .icon(Icon::Github)
- .icon_position(IconPosition::Start)
- .style(ButtonStyle::Filled)
- .full_width()
- .on_click({
- let client = self.client.clone();
- move |_, cx| {
- let client = client.clone();
- cx.spawn(move |cx| async move {
- client
- .authenticate_and_connect(true, &cx)
- .log_err()
- .await;
- })
- .detach()
- }
- }),
- )
- .child(
- div().flex().w_full().items_center().child(
- Label::new("Sign in to view notifications.")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
- )
- } else if self.notification_list.item_count() == 0 {
- this.child(
- v_stack().p_4().child(
- div().flex().w_full().items_center().child(
- Label::new("You have no notifications.")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
- )
- } else {
- this.child(list(self.notification_list.clone()).size_full())
- }
- })
- }
-}
-
-impl FocusableView for NotificationPanel {
- fn focus_handle(&self, _: &AppContext) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl EventEmitter<Event> for NotificationPanel {}
-impl EventEmitter<PanelEvent> for NotificationPanel {}
-
-impl Panel for NotificationPanel {
- fn persistent_name() -> &'static str {
- "NotificationPanel"
- }
-
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- NotificationPanelSettings::get_global(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) -> Pixels {
- self.width
- .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, 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(&self, cx: &gpui::WindowContext) -> Option<Icon> {
- (NotificationPanelSettings::get_global(cx).button
- && self.notification_store.read(cx).notification_count() > 0)
- .then(|| Icon::Bell)
- }
-
- fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
- Some("Notification Panel")
- }
-
- 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 toggle_action(&self) -> Box<dyn gpui::Action> {
- Box::new(ToggleFocus)
- }
-}
-
-pub struct NotificationToast {
- notification_id: u64,
- actor: Option<Arc<User>>,
- text: String,
- workspace: WeakView<Workspace>,
-}
-
-impl NotificationToast {
- fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
- let workspace = self.workspace.clone();
- let notification_id = self.notification_id;
- cx.window_context().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 Render for NotificationToast {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let user = self.actor.clone();
-
- h_stack()
- .id("notification_panel_toast")
- .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
- .child(Label::new(self.text.clone()))
- .child(
- IconButton::new("close", Icon::Close)
- .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
- )
- .on_click(cx.listener(|this, _, cx| {
- this.focus_notification_panel(cx);
- cx.emit(DismissEvent);
- }))
- }
-}
-
-impl EventEmitter<DismissEvent> for NotificationToast {}
-
-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())
- }
-}
@@ -1,11 +0,0 @@
-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);
-}
@@ -1,163 +0,0 @@
-use crate::notification_window_options;
-use call::{ActiveCall, IncomingCall};
-use futures::StreamExt;
-use gpui::{
- img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
- VisualContext as _, WindowHandle,
-};
-use settings::Settings;
-use std::sync::{Arc, Weak};
-use theme::ThemeSettings;
-use ui::prelude::*;
-use ui::{h_stack, v_stack, Button, Label};
-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
- .update(&mut cx, |_, cx| {
- // todo!()
- cx.remove_window();
- })
- .log_err();
- }
-
- if let Some(incoming_call) = incoming_call {
- let unique_screens = cx.update(|cx| cx.displays()).unwrap();
- let window_size = gpui::Size {
- width: px(380.),
- height: px(64.),
- };
-
- for screen in unique_screens {
- let options = notification_window_options(screen, window_size);
- let window = cx
- .open_window(options, |cx| {
- cx.new_view(|_| {
- IncomingCallNotification::new(
- incoming_call.clone(),
- app_state.clone(),
- )
- })
- })
- .unwrap();
- notification_windows.push(window);
- }
- }
- }
- })
- .detach();
-}
-
-#[derive(Clone, PartialEq)]
-struct RespondToCall {
- accept: bool,
-}
-
-struct IncomingCallNotificationState {
- call: IncomingCall,
- app_state: Weak<AppState>,
-}
-
-pub struct IncomingCallNotification {
- state: Arc<IncomingCallNotificationState>,
-}
-impl IncomingCallNotificationState {
- pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
- Self { call, app_state }
- }
-
- fn respond(&self, accept: bool, cx: &mut AppContext) {
- 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();
- let cx: &mut AppContext = cx;
- cx.spawn(|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);
- }
- })
- .log_err();
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- } else {
- active_call.update(cx, |active_call, cx| {
- active_call.decline_incoming(cx).log_err();
- });
- }
- }
-}
-
-impl IncomingCallNotification {
- pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
- Self {
- state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
- }
- }
-}
-
-impl Render for IncomingCallNotification {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- // TODO: Is there a better place for us to initialize the font?
- let (ui_font, ui_font_size) = {
- let theme_settings = ThemeSettings::get_global(cx);
- (
- theme_settings.ui_font.family.clone(),
- theme_settings.ui_font_size.clone(),
- )
- };
-
- cx.set_rem_size(ui_font_size);
-
- h_stack()
- .font(ui_font)
- .text_ui()
- .justify_between()
- .size_full()
- .overflow_hidden()
- .elevation_3(cx)
- .p_2()
- .gap_2()
- .child(
- img(self.state.call.calling_user.avatar_uri.clone())
- .w_12()
- .h_12()
- .rounded_full(),
- )
- .child(v_stack().overflow_hidden().child(Label::new(format!(
- "{} is sharing a project in Zed",
- self.state.call.calling_user.github_login
- ))))
- .child(
- v_stack()
- .child(Button::new("accept", "Accept").render(cx).on_click({
- let state = self.state.clone();
- move |_, cx| state.respond(true, cx)
- }))
- .child(Button::new("decline", "Decline").render(cx).on_click({
- let state = self.state.clone();
- move |_, cx| state.respond(false, cx)
- })),
- )
- }
-}
@@ -1,180 +0,0 @@
-use crate::notification_window_options;
-use call::{room, ActiveCall};
-use client::User;
-use collections::HashMap;
-use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
-use settings::Settings;
-use std::sync::{Arc, Weak};
-use theme::ThemeSettings;
-use ui::{h_stack, prelude::*, v_stack, Button, Label};
-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 window_size = Size {
- width: px(400.),
- height: px(72.),
- };
-
- for screen in cx.displays() {
- let options = notification_window_options(screen, window_size);
- let window = cx.open_window(options, |cx| {
- cx.new_view(|_| {
- 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
- .update(cx, |_, cx| {
- // todo!()
- cx.remove_window();
- })
- .ok();
- }
- }
- }
-
- room::Event::Left => {
- for (_, windows) in notification_windows.drain() {
- for window in windows {
- window
- .update(cx, |_, cx| {
- // todo!()
- cx.remove_window();
- })
- .ok();
- }
- }
- }
- _ => {}
- })
- .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,
- });
- });
- }
- }
-}
-
-impl Render for ProjectSharedNotification {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- // TODO: Is there a better place for us to initialize the font?
- let (ui_font, ui_font_size) = {
- let theme_settings = ThemeSettings::get_global(cx);
- (
- theme_settings.ui_font.family.clone(),
- theme_settings.ui_font_size.clone(),
- )
- };
-
- cx.set_rem_size(ui_font_size);
-
- h_stack()
- .font(ui_font)
- .text_ui()
- .justify_between()
- .size_full()
- .overflow_hidden()
- .elevation_3(cx)
- .p_2()
- .gap_2()
- .child(
- img(self.owner.avatar_uri.clone())
- .w_12()
- .h_12()
- .rounded_full(),
- )
- .child(
- v_stack()
- .overflow_hidden()
- .child(Label::new(self.owner.github_login.clone()))
- .child(Label::new(format!(
- "is sharing a project in Zed{}",
- if self.worktree_root_names.is_empty() {
- ""
- } else {
- ":"
- }
- )))
- .children(if self.worktree_root_names.is_empty() {
- None
- } else {
- Some(Label::new(self.worktree_root_names.join(", ")))
- }),
- )
- .child(
- v_stack()
- .child(Button::new("open", "Open").on_click(cx.listener(
- move |this, _event, cx| {
- this.join(cx);
- },
- )))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
- move |this, _event, cx| {
- this.dismiss(cx);
- },
- ))),
- )
- }
-}
@@ -1,70 +0,0 @@
-use anyhow;
-use gpui::Pixels;
-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: Pixels,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatPanelSettings {
- pub button: bool,
- pub dock: DockPosition,
- pub default_width: Pixels,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct NotificationPanelSettings {
- pub button: bool,
- pub dock: DockPosition,
- pub default_width: Pixels,
-}
-
-#[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)
- }
-}
@@ -9,15 +9,14 @@ path = "src/quick_action_bar.rs"
doctest = false
[dependencies]
-assistant = { path = "../assistant" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-search = { path = "../search" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+assistant = { package = "assistant2", path = "../assistant2" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+search = { package = "search2", path = "../search2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+ui = { package = "ui2", path = "../ui2" }
[dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-theme = { path = "../theme", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
@@ -1,155 +1,153 @@
-use assistant::{assistant_panel::InlineAssist, AssistantPanel};
+use assistant::{AssistantPanel, InlineAssist};
use editor::Editor;
+
use gpui::{
- elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
- platform::{CursorStyle, MouseButton},
- Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
- WeakViewHandle,
+ Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
+ Subscription, View, ViewContext, WeakView,
};
-
use search::{buffer_search, BufferSearchBar};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
+use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use workspace::{
+ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
pub struct QuickActionBar {
- buffer_search_bar: ViewHandle<BufferSearchBar>,
+ buffer_search_bar: View<BufferSearchBar>,
active_item: Option<Box<dyn ItemHandle>>,
- inlay_hints_enabled_subscription: Option<Subscription>,
- workspace: WeakViewHandle<Workspace>,
+ _inlay_hints_enabled_subscription: Option<Subscription>,
+ workspace: WeakView<Workspace>,
}
impl QuickActionBar {
- pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
+ pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
Self {
buffer_search_bar,
active_item: None,
- inlay_hints_enabled_subscription: None,
+ _inlay_hints_enabled_subscription: None,
workspace: workspace.weak_handle(),
}
}
- fn active_editor(&self) -> Option<ViewHandle<Editor>> {
+ fn active_editor(&self) -> Option<View<Editor>> {
self.active_item
.as_ref()
.and_then(|item| item.downcast::<Editor>())
}
}
-impl Entity for QuickActionBar {
- type Event = ();
-}
-
-impl View for QuickActionBar {
- fn ui_name() -> &'static str {
- "QuickActionsBar"
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+impl Render for QuickActionBar {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(editor) = self.active_editor() else {
- return Empty::new().into_any();
+ return div().id("empty quick action bar");
};
- let mut bar = Flex::row();
- if editor.read(cx).supports_inlay_hints(cx) {
- bar = bar.with_child(render_quick_action_bar_button(
- 0,
- "icons/inlay_hint.svg",
- editor.read(cx).inlay_hints_enabled(),
- (
- "Toggle Inlay Hints".to_string(),
- Some(Box::new(editor::ToggleInlayHints)),
- ),
- cx,
- |this, cx| {
- if let Some(editor) = this.active_editor() {
- editor.update(cx, |editor, cx| {
- editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
- });
- }
- },
- ));
- }
-
- if editor.read(cx).buffer().read(cx).is_singleton() {
- let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
- let search_action = buffer_search::Deploy { focus: true };
-
- bar = bar.with_child(render_quick_action_bar_button(
- 1,
- "icons/magnifying_glass.svg",
- search_bar_shown,
- (
- "Buffer Search".to_string(),
- Some(Box::new(search_action.clone())),
- ),
- cx,
- move |this, cx| {
- this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
- if search_bar_shown {
- buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
- } else {
- buffer_search_bar.deploy(&search_action, cx);
- }
+ let inlay_hints_button = Some(QuickActionBarButton::new(
+ "toggle inlay hints",
+ Icon::InlayHint,
+ editor.read(cx).inlay_hints_enabled(),
+ Box::new(editor::ToggleInlayHints),
+ "Toggle Inlay Hints",
+ {
+ let editor = editor.clone();
+ move |_, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
});
- },
- ));
- }
-
- bar.add_child(render_quick_action_bar_button(
- 2,
- "icons/magic-wand.svg",
- false,
- ("Inline Assist".into(), Some(Box::new(InlineAssist))),
- cx,
- move |this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- AssistantPanel::inline_assist(workspace, &Default::default(), cx);
+ }
+ },
+ ))
+ .filter(|_| editor.read(cx).supports_inlay_hints(cx));
+
+ let search_button = Some(QuickActionBarButton::new(
+ "toggle buffer search",
+ Icon::MagnifyingGlass,
+ !self.buffer_search_bar.read(cx).is_dismissed(),
+ Box::new(buffer_search::Deploy { focus: false }),
+ "Buffer Search",
+ {
+ let buffer_search_bar = self.buffer_search_bar.clone();
+ move |_, cx| {
+ buffer_search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle(&buffer_search::Deploy { focus: true }, cx)
});
}
},
- ));
+ ))
+ .filter(|_| editor.is_singleton(cx));
- bar.into_any()
+ let assistant_button = QuickActionBarButton::new(
+ "toggle inline assistant",
+ Icon::MagicWand,
+ false,
+ Box::new(InlineAssist),
+ "Inline Assist",
+ {
+ let workspace = self.workspace.clone();
+ move |_, cx| {
+ if let Some(workspace) = workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
+ });
+ }
+ }
+ },
+ );
+
+ h_stack()
+ .id("quick action bar")
+ .p_1()
+ .gap_2()
+ .children(inlay_hints_button)
+ .children(search_button)
+ .child(assistant_button)
}
}
-fn render_quick_action_bar_button<
- F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
->(
- index: usize,
- icon: &'static str,
+impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
+
+#[derive(IntoElement)]
+struct QuickActionBarButton {
+ id: ElementId,
+ icon: Icon,
toggled: bool,
- tooltip: (String, Option<Box<dyn Action>>),
- cx: &mut ViewContext<QuickActionBar>,
- on_click: F,
-) -> AnyElement<QuickActionBar> {
- enum QuickActionBarButton {}
-
- let theme = theme::current(cx);
- let (tooltip_text, action) = tooltip;
-
- MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
- let style = theme
- .workspace
- .toolbar
- .toggleable_tool
- .in_state(toggled)
- .style_for(mouse_state);
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
- .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
- .into_any_named("quick action bar button")
+ action: Box<dyn Action>,
+ tooltip: SharedString,
+ on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
+}
+
+impl QuickActionBarButton {
+ fn new(
+ id: impl Into<ElementId>,
+ icon: Icon,
+ toggled: bool,
+ action: Box<dyn Action>,
+ tooltip: impl Into<SharedString>,
+ on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ Self {
+ id: id.into(),
+ icon,
+ toggled,
+ action,
+ tooltip: tooltip.into(),
+ on_click: Box::new(on_click),
+ }
+ }
+}
+
+impl RenderOnce for QuickActionBarButton {
+ fn render(self, _: &mut WindowContext) -> impl IntoElement {
+ let tooltip = self.tooltip.clone();
+ let action = self.action.boxed_clone();
+
+ IconButton::new(self.id.clone(), self.icon)
+ .size(ButtonSize::Compact)
+ .icon_size(IconSize::Small)
+ .style(ButtonStyle::Subtle)
+ .selected(self.toggled)
+ .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+ .on_click(move |event, cx| (self.on_click)(event, cx))
+ }
}
impl ToolbarItemView for QuickActionBar {
@@ -161,12 +159,12 @@ impl ToolbarItemView for QuickActionBar {
match active_pane_item {
Some(active_item) => {
self.active_item = Some(active_item.boxed_clone());
- self.inlay_hints_enabled_subscription.take();
+ self._inlay_hints_enabled_subscription.take();
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
- self.inlay_hints_enabled_subscription =
+ self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let editor = editor.read(cx);
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
@@ -179,7 +177,7 @@ impl ToolbarItemView for QuickActionBar {
cx.notify()
}
}));
- ToolbarItemLocation::PrimaryRight { flex: None }
+ ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
@@ -1,22 +0,0 @@
-[package]
-name = "quick_action_bar2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/quick_action_bar.rs"
-doctest = false
-
-[dependencies]
-assistant = { package = "assistant2", path = "../assistant2" }
-editor = { package = "editor2", path = "../editor2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-search = { package = "search2", path = "../search2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-ui = { package = "ui2", path = "../ui2" }
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
@@ -1,191 +0,0 @@
-use assistant::{AssistantPanel, InlineAssist};
-use editor::Editor;
-
-use gpui::{
- Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
- Subscription, View, ViewContext, WeakView,
-};
-use search::{buffer_search, BufferSearchBar};
-use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
-use workspace::{
- item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
-
-pub struct QuickActionBar {
- buffer_search_bar: View<BufferSearchBar>,
- active_item: Option<Box<dyn ItemHandle>>,
- _inlay_hints_enabled_subscription: Option<Subscription>,
- workspace: WeakView<Workspace>,
-}
-
-impl QuickActionBar {
- pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
- Self {
- buffer_search_bar,
- active_item: None,
- _inlay_hints_enabled_subscription: None,
- workspace: workspace.weak_handle(),
- }
- }
-
- fn active_editor(&self) -> Option<View<Editor>> {
- self.active_item
- .as_ref()
- .and_then(|item| item.downcast::<Editor>())
- }
-}
-
-impl Render for QuickActionBar {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let Some(editor) = self.active_editor() else {
- return div().id("empty quick action bar");
- };
-
- let inlay_hints_button = Some(QuickActionBarButton::new(
- "toggle inlay hints",
- Icon::InlayHint,
- editor.read(cx).inlay_hints_enabled(),
- Box::new(editor::ToggleInlayHints),
- "Toggle Inlay Hints",
- {
- let editor = editor.clone();
- move |_, cx| {
- editor.update(cx, |editor, cx| {
- editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
- });
- }
- },
- ))
- .filter(|_| editor.read(cx).supports_inlay_hints(cx));
-
- let search_button = Some(QuickActionBarButton::new(
- "toggle buffer search",
- Icon::MagnifyingGlass,
- !self.buffer_search_bar.read(cx).is_dismissed(),
- Box::new(buffer_search::Deploy { focus: false }),
- "Buffer Search",
- {
- let buffer_search_bar = self.buffer_search_bar.clone();
- move |_, cx| {
- buffer_search_bar.update(cx, |search_bar, cx| {
- search_bar.toggle(&buffer_search::Deploy { focus: true }, cx)
- });
- }
- },
- ))
- .filter(|_| editor.is_singleton(cx));
-
- let assistant_button = QuickActionBarButton::new(
- "toggle inline assistant",
- Icon::MagicWand,
- false,
- Box::new(InlineAssist),
- "Inline Assist",
- {
- let workspace = self.workspace.clone();
- move |_, cx| {
- if let Some(workspace) = workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
- });
- }
- }
- },
- );
-
- h_stack()
- .id("quick action bar")
- .p_1()
- .gap_2()
- .children(inlay_hints_button)
- .children(search_button)
- .child(assistant_button)
- }
-}
-
-impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
-
-#[derive(IntoElement)]
-struct QuickActionBarButton {
- id: ElementId,
- icon: Icon,
- toggled: bool,
- action: Box<dyn Action>,
- tooltip: SharedString,
- on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
-}
-
-impl QuickActionBarButton {
- fn new(
- id: impl Into<ElementId>,
- icon: Icon,
- toggled: bool,
- action: Box<dyn Action>,
- tooltip: impl Into<SharedString>,
- on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
- ) -> Self {
- Self {
- id: id.into(),
- icon,
- toggled,
- action,
- tooltip: tooltip.into(),
- on_click: Box::new(on_click),
- }
- }
-}
-
-impl RenderOnce for QuickActionBarButton {
- fn render(self, _: &mut WindowContext) -> impl IntoElement {
- let tooltip = self.tooltip.clone();
- let action = self.action.boxed_clone();
-
- IconButton::new(self.id.clone(), self.icon)
- .size(ButtonSize::Compact)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle)
- .selected(self.toggled)
- .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
- .on_click(move |event, cx| (self.on_click)(event, cx))
- }
-}
-
-impl ToolbarItemView for QuickActionBar {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> ToolbarItemLocation {
- match active_pane_item {
- Some(active_item) => {
- self.active_item = Some(active_item.boxed_clone());
- self._inlay_hints_enabled_subscription.take();
-
- if let Some(editor) = active_item.downcast::<Editor>() {
- let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
- let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
- self._inlay_hints_enabled_subscription =
- Some(cx.observe(&editor, move |_, editor, cx| {
- let editor = editor.read(cx);
- let new_inlay_hints_enabled = editor.inlay_hints_enabled();
- let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
- let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
- || supports_inlay_hints != new_supports_inlay_hints;
- inlay_hints_enabled = new_inlay_hints_enabled;
- supports_inlay_hints = new_supports_inlay_hints;
- if should_notify {
- cx.notify()
- }
- }));
- ToolbarItemLocation::PrimaryRight
- } else {
- ToolbarItemLocation::Hidden
- }
- }
- None => {
- self.active_item = None;
- ToolbarItemLocation::Hidden
- }
- }
- }
-}
@@ -6,12 +6,12 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-fuzzy = {path = "../fuzzy"}
-fs = {path = "../fs"}
-gpui = {path = "../gpui"}
-picker = {path = "../picker"}
+fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
+fs = {package = "fs2", path = "../fs2"}
+gpui = {package = "gpui2", path = "../gpui2"}
+picker = {package = "picker2", path = "../picker2"}
util = {path = "../util"}
-theme = {path = "../theme"}
-workspace = {path = "../workspace"}
+ui = {package = "ui2", path = "../ui2"}
+workspace = {package = "workspace2", path = "../workspace2"}
anyhow.workspace = true
@@ -2,57 +2,95 @@ use anyhow::{anyhow, bail, Result};
use fs::repository::Branch;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- actions,
- elements::*,
- platform::{CursorStyle, MouseButton},
- AppContext, MouseState, Task, ViewContext, ViewHandle,
+ actions, rems, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
+ FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
+ Subscription, Task, View, ViewContext, VisualContext, WindowContext,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use std::{ops::Not, sync::Arc};
+use ui::{
+ h_stack, v_stack, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon,
+ LabelSize, ListItem, ListItemSpacing, Selectable,
+};
use util::ResultExt;
-use workspace::{Toast, Workspace};
+use workspace::{ModalView, Toast, Workspace};
actions!(branches, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
- Picker::<BranchListDelegate>::init(cx);
- cx.add_action(toggle);
+ // todo!() po
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, action, cx| {
+ BranchList::toggle_modal(workspace, action, cx).log_err();
+ });
+ })
+ .detach();
}
-pub type BranchList = Picker<BranchListDelegate>;
-pub fn build_branch_list(
- workspace: ViewHandle<Workspace>,
- cx: &mut ViewContext<BranchList>,
-) -> Result<BranchList> {
- let delegate = workspace.read_with(cx, |workspace, cx| {
- BranchListDelegate::new(workspace, cx.handle(), 29, cx)
- })?;
+pub struct BranchList {
+ pub picker: View<Picker<BranchListDelegate>>,
+ rem_width: f32,
+ _subscription: Subscription,
+}
+
+impl BranchList {
+ fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
+ let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+ let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
+ Self {
+ picker,
+ rem_width,
+ _subscription,
+ }
+ }
+ fn toggle_modal(
+ workspace: &mut Workspace,
+ _: &OpenRecent,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Result<()> {
+ // Modal branch picker has a longer trailoff than a popover one.
+ let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
+ workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
- Ok(Picker::new(delegate, cx).with_theme(|theme| theme.picker.clone()))
+ Ok(())
+ }
}
+impl ModalView for BranchList {}
+impl EventEmitter<DismissEvent> for BranchList {}
-fn toggle(
- workspace: &mut Workspace,
- _: &OpenRecent,
- cx: &mut ViewContext<Workspace>,
-) -> Result<()> {
- // Modal branch picker has a longer trailoff than a popover one.
- let delegate = BranchListDelegate::new(workspace, cx.handle(), 70, cx)?;
- workspace.toggle_modal(cx, |_, cx| {
- cx.add_view(|cx| {
- Picker::new(delegate, cx)
- .with_theme(|theme| theme.picker.clone())
- .with_max_size(800., 1200.)
- })
- });
+impl FocusableView for BranchList {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
- Ok(())
+impl Render for BranchList {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_stack()
+ .w(rems(self.rem_width))
+ .child(self.picker.clone())
+ .on_mouse_down_out(cx.listener(|this, _, cx| {
+ this.picker.update(cx, |this, cx| {
+ this.cancel(&Default::default(), cx);
+ })
+ }))
+ }
+}
+
+pub fn build_branch_list(
+ workspace: View<Workspace>,
+ cx: &mut WindowContext<'_>,
+) -> Result<View<BranchList>> {
+ let delegate = workspace.update(cx, |workspace, cx| {
+ BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
+ })?;
+ Ok(cx.new_view(move |cx| BranchList::new(delegate, 20., cx)))
}
pub struct BranchListDelegate {
matches: Vec<StringMatch>,
all_branches: Vec<Branch>,
- workspace: ViewHandle<Workspace>,
+ workspace: View<Workspace>,
selected_index: usize,
last_query: String,
/// Max length of branch name before we truncate it and add a trailing `...`.
@@ -62,7 +100,7 @@ pub struct BranchListDelegate {
impl BranchListDelegate {
fn new(
workspace: &Workspace,
- handle: ViewHandle<Workspace>,
+ handle: View<Workspace>,
branch_name_trailoff_after: usize,
cx: &AppContext,
) -> Result<Self> {
@@ -87,7 +125,7 @@ impl BranchListDelegate {
})
}
- fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
+ fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
self.workspace.update(cx, |model, ctx| {
model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
@@ -96,6 +134,8 @@ impl BranchListDelegate {
}
impl PickerDelegate for BranchListDelegate {
+ type ListItem = ListItem;
+
fn placeholder_text(&self) -> Arc<str> {
"Select branch...".into()
}
@@ -114,9 +154,9 @@ impl PickerDelegate for BranchListDelegate {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
cx.spawn(move |picker, mut cx| async move {
- let candidates = picker.read_with(&mut cx, |view, _| {
+ let candidates = picker.update(&mut cx, |view, _| {
const RECENT_BRANCHES_COUNT: usize = 10;
- let mut branches = view.delegate().all_branches.clone();
+ let mut branches = view.delegate.all_branches.clone();
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
@@ -157,13 +197,13 @@ impl PickerDelegate for BranchListDelegate {
true,
10000,
&Default::default(),
- cx.background(),
+ cx.background_executor().clone(),
)
.await
};
picker
.update(&mut cx, |picker, _| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
@@ -189,7 +229,7 @@ impl PickerDelegate for BranchListDelegate {
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |this, cx| {
- let project = this.delegate().workspace.read(cx).project().read(cx);
+ let project = this.delegate.workspace.read(cx).project().read(cx);
let mut cwd = project
.visible_worktrees(cx)
.next()
@@ -210,10 +250,10 @@ impl PickerDelegate for BranchListDelegate {
.lock()
.change_branch(¤t_pick);
if status.is_err() {
- this.delegate().display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
status?;
}
- cx.emit(PickerEvent::Dismiss);
+ cx.emit(DismissEvent);
Ok::<(), anyhow::Error>(())
})
@@ -223,123 +263,96 @@ impl PickerDelegate for BranchListDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> AnyElement<Picker<Self>> {
- let theme = &theme::current(cx);
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let shortened_branch_name =
util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
- let highlights = hit
+ let highlights: Vec<_> = hit
.positions
.iter()
+ .filter(|index| index < &&self.branch_name_trailoff_after)
.copied()
- .filter(|index| index < &self.branch_name_trailoff_after)
.collect();
- let style = theme.picker.item.in_state(selected).style_for(mouse_state);
- Flex::row()
- .with_child(
- Label::new(shortened_branch_name.clone(), style.label.clone())
- .with_highlights(highlights)
- .contained()
- .aligned()
- .left(),
- )
- .contained()
- .with_style(style.container)
- .constrained()
- .with_height(theme.collab_panel.tabbed_modal.row_height)
- .into_any()
+ Some(
+ ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .selected(selected)
+ .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)),
+ )
}
- fn render_header(
- &self,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<AnyElement<Picker<Self>>> {
- let theme = &theme::current(cx);
- let style = theme.picker.header.clone();
+ fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let label = if self.last_query.is_empty() {
- Flex::row()
- .with_child(Label::new("Recent branches", style.label.clone()))
- .contained()
- .with_style(style.container)
+ h_stack()
+ .ml_3()
+ .child(Label::new("Recent branches").size(LabelSize::Small))
} else {
- Flex::row()
- .with_child(Label::new("Branches", style.label.clone()))
- .with_children(self.matches.is_empty().not().then(|| {
- let suffix = if self.matches.len() == 1 { "" } else { "es" };
- Label::new(
- format!("{} match{}", self.matches.len(), suffix),
- style.label,
- )
- .flex_float()
- }))
- .contained()
- .with_style(style.container)
+ let match_label = self.matches.is_empty().not().then(|| {
+ let suffix = if self.matches.len() == 1 { "" } else { "es" };
+ Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small)
+ });
+ h_stack()
+ .px_3()
+ .h_full()
+ .justify_between()
+ .child(Label::new("Branches").size(LabelSize::Small))
+ .children(match_label)
};
Some(label.into_any())
}
- fn render_footer(
- &self,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<AnyElement<Picker<Self>>> {
- if !self.last_query.is_empty() {
- let theme = &theme::current(cx);
- let style = theme.picker.footer.clone();
- enum BranchCreateButton {}
- Some(
- Flex::row().with_child(MouseEventHandler::new::<BranchCreateButton, _>(0, cx, |state, _| {
- let style = style.style_for(state);
- Label::new("Create branch", style.label.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, |_, _, cx| {
- cx.spawn(|picker, mut cx| async move {
- picker.update(&mut cx, |this, cx| {
- let project = this.delegate().workspace.read(cx).project().read(cx);
- let current_pick = &this.delegate().last_query;
- let mut cwd = project
- .visible_worktrees(cx)
- .next()
- .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
- .read(cx)
- .abs_path()
- .to_path_buf();
- cwd.push(".git");
- let repo = project
- .fs()
- .open_repo(&cwd)
- .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
- let repo = repo
- .lock();
- let status = repo
- .create_branch(¤t_pick);
- if status.is_err() {
- this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
- status?;
- }
- let status = repo.change_branch(¤t_pick);
- if status.is_err() {
- this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
- status?;
- }
- cx.emit(PickerEvent::Dismiss);
- Ok::<(), anyhow::Error>(())
- })
- }).detach();
- })).aligned().right()
- .into_any(),
- )
- } else {
- None
+ fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
+ if self.last_query.is_empty() {
+ return None;
}
+
+ Some(
+ h_stack().mr_3().pb_2().child(h_stack().w_full()).child(
+ Button::new("branch-picker-create-branch-button", "Create branch").on_click(
+ cx.listener(|_, _, cx| {
+ cx.spawn(|picker, mut cx| async move {
+ picker.update(&mut cx, |this, cx| {
+ let project = this.delegate.workspace.read(cx).project().read(cx);
+ let current_pick = &this.delegate.last_query;
+ let mut cwd = project
+ .visible_worktrees(cx)
+ .next()
+ .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let repo = project
+ .fs()
+ .open_repo(&cwd)
+ .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
+ let repo = repo
+ .lock();
+ let status = repo
+ .create_branch(¤t_pick);
+ if status.is_err() {
+ this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ status?;
+ }
+ let status = repo.change_branch(¤t_pick);
+ if status.is_err() {
+ this.delegate.display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
+ status?;
+ }
+ this.cancel(&Default::default(), cx);
+ Ok::<(), anyhow::Error>(())
+ })
+
+ }).detach_and_log_err(cx);
+ }),
+ ).style(ui::ButtonStyle::Filled)).into_any_element(),
+ )
}
}
@@ -1,17 +0,0 @@
-[package]
-name = "vcs_menu2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
-fs = {package = "fs2", path = "../fs2"}
-gpui = {package = "gpui2", path = "../gpui2"}
-picker = {package = "picker2", path = "../picker2"}
-util = {path = "../util"}
-ui = {package = "ui2", path = "../ui2"}
-workspace = {package = "workspace2", path = "../workspace2"}
-
-anyhow.workspace = true
@@ -1,358 +0,0 @@
-use anyhow::{anyhow, bail, Result};
-use fs::repository::Branch;
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
- actions, rems, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
- FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
- Subscription, Task, View, ViewContext, VisualContext, WindowContext,
-};
-use picker::{Picker, PickerDelegate};
-use std::{ops::Not, sync::Arc};
-use ui::{
- h_stack, v_stack, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon,
- LabelSize, ListItem, ListItemSpacing, Selectable,
-};
-use util::ResultExt;
-use workspace::{ModalView, Toast, Workspace};
-
-actions!(branches, [OpenRecent]);
-
-pub fn init(cx: &mut AppContext) {
- // todo!() po
- cx.observe_new_views(|workspace: &mut Workspace, _| {
- workspace.register_action(|workspace, action, cx| {
- BranchList::toggle_modal(workspace, action, cx).log_err();
- });
- })
- .detach();
-}
-
-pub struct BranchList {
- pub picker: View<Picker<BranchListDelegate>>,
- rem_width: f32,
- _subscription: Subscription,
-}
-
-impl BranchList {
- fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
- let picker = cx.new_view(|cx| Picker::new(delegate, cx));
- let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
- Self {
- picker,
- rem_width,
- _subscription,
- }
- }
- fn toggle_modal(
- workspace: &mut Workspace,
- _: &OpenRecent,
- cx: &mut ViewContext<Workspace>,
- ) -> Result<()> {
- // Modal branch picker has a longer trailoff than a popover one.
- let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
- workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
-
- Ok(())
- }
-}
-impl ModalView for BranchList {}
-impl EventEmitter<DismissEvent> for BranchList {}
-
-impl FocusableView for BranchList {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl Render for BranchList {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack()
- .w(rems(self.rem_width))
- .child(self.picker.clone())
- .on_mouse_down_out(cx.listener(|this, _, cx| {
- this.picker.update(cx, |this, cx| {
- this.cancel(&Default::default(), cx);
- })
- }))
- }
-}
-
-pub fn build_branch_list(
- workspace: View<Workspace>,
- cx: &mut WindowContext<'_>,
-) -> Result<View<BranchList>> {
- let delegate = workspace.update(cx, |workspace, cx| {
- BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
- })?;
- Ok(cx.new_view(move |cx| BranchList::new(delegate, 20., cx)))
-}
-
-pub struct BranchListDelegate {
- matches: Vec<StringMatch>,
- all_branches: Vec<Branch>,
- workspace: View<Workspace>,
- selected_index: usize,
- last_query: String,
- /// Max length of branch name before we truncate it and add a trailing `...`.
- branch_name_trailoff_after: usize,
-}
-
-impl BranchListDelegate {
- fn new(
- workspace: &Workspace,
- handle: View<Workspace>,
- branch_name_trailoff_after: usize,
- cx: &AppContext,
- ) -> Result<Self> {
- let project = workspace.project().read(&cx);
- let Some(worktree) = project.visible_worktrees(cx).next() else {
- bail!("Cannot update branch list as there are no visible worktrees")
- };
-
- let mut cwd = worktree.read(cx).abs_path().to_path_buf();
- cwd.push(".git");
- let Some(repo) = project.fs().open_repo(&cwd) else {
- bail!("Project does not have associated git repository.")
- };
- let all_branches = repo.lock().branches()?;
- Ok(Self {
- matches: vec![],
- workspace: handle,
- all_branches,
- selected_index: 0,
- last_query: Default::default(),
- branch_name_trailoff_after,
- })
- }
-
- fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
- const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
- self.workspace.update(cx, |model, ctx| {
- model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
- });
- }
-}
-
-impl PickerDelegate for BranchListDelegate {
- type ListItem = ListItem;
-
- fn placeholder_text(&self) -> Arc<str> {
- "Select branch...".into()
- }
-
- fn match_count(&self) -> usize {
- self.matches.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<()> {
- cx.spawn(move |picker, mut cx| async move {
- let candidates = picker.update(&mut cx, |view, _| {
- const RECENT_BRANCHES_COUNT: usize = 10;
- let mut branches = view.delegate.all_branches.clone();
- if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
- // Truncate list of recent branches
- // Do a partial sort to show recent-ish branches first.
- branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
- rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
- });
- branches.truncate(RECENT_BRANCHES_COUNT);
- branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
- }
- branches
- .into_iter()
- .enumerate()
- .map(|(ix, command)| StringMatchCandidate {
- id: ix,
- char_bag: command.name.chars().collect(),
- string: command.name.into(),
- })
- .collect::<Vec<StringMatchCandidate>>()
- });
- let Some(candidates) = candidates.log_err() else {
- return;
- };
- let matches = if query.is_empty() {
- candidates
- .into_iter()
- .enumerate()
- .map(|(index, candidate)| StringMatch {
- candidate_id: index,
- string: candidate.string,
- positions: Vec::new(),
- score: 0.0,
- })
- .collect()
- } else {
- fuzzy::match_strings(
- &candidates,
- &query,
- true,
- 10000,
- &Default::default(),
- cx.background_executor().clone(),
- )
- .await
- };
- picker
- .update(&mut cx, |picker, _| {
- let delegate = &mut picker.delegate;
- delegate.matches = matches;
- if delegate.matches.is_empty() {
- delegate.selected_index = 0;
- } else {
- delegate.selected_index =
- core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
- }
- delegate.last_query = query;
- })
- .log_err();
- })
- }
-
- fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
- let current_pick = self.selected_index();
- let Some(current_pick) = self
- .matches
- .get(current_pick)
- .map(|pick| pick.string.clone())
- else {
- return;
- };
- cx.spawn(|picker, mut cx| async move {
- picker
- .update(&mut cx, |this, cx| {
- let project = this.delegate.workspace.read(cx).project().read(cx);
- let mut cwd = project
- .visible_worktrees(cx)
- .next()
- .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
- .read(cx)
- .abs_path()
- .to_path_buf();
- cwd.push(".git");
- let status = project
- .fs()
- .open_repo(&cwd)
- .ok_or_else(|| {
- anyhow!(
- "Could not open repository at path `{}`",
- cwd.as_os_str().to_string_lossy()
- )
- })?
- .lock()
- .change_branch(¤t_pick);
- if status.is_err() {
- this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
- status?;
- }
- cx.emit(DismissEvent);
-
- Ok::<(), anyhow::Error>(())
- })
- .log_err();
- })
- .detach();
- }
-
- fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(DismissEvent);
- }
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let hit = &self.matches[ix];
- let shortened_branch_name =
- util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
- let highlights: Vec<_> = hit
- .positions
- .iter()
- .filter(|index| index < &&self.branch_name_trailoff_after)
- .copied()
- .collect();
- Some(
- ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .selected(selected)
- .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)),
- )
- }
- fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
- let label = if self.last_query.is_empty() {
- h_stack()
- .ml_3()
- .child(Label::new("Recent branches").size(LabelSize::Small))
- } else {
- let match_label = self.matches.is_empty().not().then(|| {
- let suffix = if self.matches.len() == 1 { "" } else { "es" };
- Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small)
- });
- h_stack()
- .px_3()
- .h_full()
- .justify_between()
- .child(Label::new("Branches").size(LabelSize::Small))
- .children(match_label)
- };
- Some(label.into_any())
- }
- fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
- if self.last_query.is_empty() {
- return None;
- }
-
- Some(
- h_stack().mr_3().pb_2().child(h_stack().w_full()).child(
- Button::new("branch-picker-create-branch-button", "Create branch").on_click(
- cx.listener(|_, _, cx| {
- cx.spawn(|picker, mut cx| async move {
- picker.update(&mut cx, |this, cx| {
- let project = this.delegate.workspace.read(cx).project().read(cx);
- let current_pick = &this.delegate.last_query;
- let mut cwd = project
- .visible_worktrees(cx)
- .next()
- .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
- .read(cx)
- .abs_path()
- .to_path_buf();
- cwd.push(".git");
- let repo = project
- .fs()
- .open_repo(&cwd)
- .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
- let repo = repo
- .lock();
- let status = repo
- .create_branch(¤t_pick);
- if status.is_err() {
- this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
- status?;
- }
- let status = repo.change_branch(¤t_pick);
- if status.is_err() {
- this.delegate.display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
- status?;
- }
- this.cancel(&Default::default(), cx);
- Ok::<(), anyhow::Error>(())
- })
-
- }).detach_and_log_err(cx);
- }),
- ).style(ui::ButtonStyle::Filled)).into_any_element(),
- )
- }
-}
@@ -11,21 +11,22 @@ path = "src/welcome.rs"
test-support = []
[dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-db = { path = "../db" }
-install_cli = { path = "../install_cli" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+client = { package = "client2", path = "../client2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+db = { package = "db2", path = "../db2" }
+install_cli = { package = "install_cli2", path = "../install_cli2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
util = { path = "../util" }
-picker = { path = "../picker" }
-workspace = { path = "../workspace" }
-vim = { path = "../vim" }
+picker = { package = "picker2", path = "../picker2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+vim = { package = "vim2", path = "../vim2" }
anyhow.workspace = true
log.workspace = true
@@ -33,4 +34,4 @@ schemars.workspace = true
serde.workspace = true
[dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -1,22 +1,24 @@
use super::base_keymap_setting::BaseKeymap;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
- actions,
- elements::{Element as _, Label},
- AppContext, Task, ViewContext,
+ actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
+ ViewContext, VisualContext, WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use project::Fs;
-use settings::update_settings_file;
+use settings::{update_settings_file, Settings};
use std::sync::Arc;
+use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ui::HighlightedLabel, ModalView, Workspace};
actions!(welcome, [ToggleBaseKeymapSelector]);
pub fn init(cx: &mut AppContext) {
- cx.add_action(toggle);
- BaseKeymapSelector::init(cx);
+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+ workspace.register_action(toggle);
+ })
+ .detach();
}
pub fn toggle(
@@ -24,28 +26,69 @@ pub fn toggle(
_: &ToggleBaseKeymapSelector,
cx: &mut ViewContext<Workspace>,
) {
- workspace.toggle_modal(cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
+ let fs = workspace.app_state().fs.clone();
+ workspace.toggle_modal(cx, |cx| {
+ BaseKeymapSelector::new(
+ BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+ cx,
+ )
});
}
-pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
+pub struct BaseKeymapSelector {
+ focus_handle: gpui::FocusHandle,
+ picker: View<Picker<BaseKeymapSelectorDelegate>>,
+}
+
+impl FocusableView for BaseKeymapSelector {
+ fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
+impl ModalView for BaseKeymapSelector {}
+
+impl BaseKeymapSelector {
+ pub fn new(
+ delegate: BaseKeymapSelectorDelegate,
+ cx: &mut ViewContext<BaseKeymapSelector>,
+ ) -> Self {
+ let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+ let focus_handle = cx.focus_handle();
+ Self {
+ focus_handle,
+ picker,
+ }
+ }
+}
+
+impl Render for BaseKeymapSelector {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ self.picker.clone()
+ }
+}
pub struct BaseKeymapSelectorDelegate {
+ view: WeakView<BaseKeymapSelector>,
matches: Vec<StringMatch>,
selected_index: usize,
fs: Arc<dyn Fs>,
}
impl BaseKeymapSelectorDelegate {
- fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
- let base = settings::get::<BaseKeymap>(cx);
+ fn new(
+ weak_view: WeakView<BaseKeymapSelector>,
+ fs: Arc<dyn Fs>,
+ cx: &mut ViewContext<BaseKeymapSelector>,
+ ) -> Self {
+ let base = BaseKeymap::get(None, cx);
let selected_index = BaseKeymap::OPTIONS
.iter()
.position(|(_, value)| value == base)
.unwrap_or(0);
Self {
+ view: weak_view,
matches: Vec::new(),
selected_index,
fs,
@@ -54,6 +97,8 @@ impl BaseKeymapSelectorDelegate {
}
impl PickerDelegate for BaseKeymapSelectorDelegate {
+ type ListItem = ui::ListItem;
+
fn placeholder_text(&self) -> Arc<str> {
"Select a base keymap...".into()
}
@@ -66,16 +111,20 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
self.selected_index
}
- fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<BaseKeymapSelector>) {
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
+ ) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
- cx: &mut ViewContext<BaseKeymapSelector>,
+ cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
) -> Task<()> {
- let background = cx.background().clone();
+ let background = cx.background_executor().clone();
let candidates = BaseKeymap::names()
.enumerate()
.map(|(id, name)| StringMatchCandidate {
@@ -110,43 +159,50 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
};
this.update(&mut cx, |this, _| {
- let delegate = this.delegate_mut();
- delegate.matches = matches;
- delegate.selected_index = delegate
+ this.delegate.matches = matches;
+ this.delegate.selected_index = this
+ .delegate
.selected_index
- .min(delegate.matches.len().saturating_sub(1));
+ .min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
- fn confirm(&mut self, _: bool, cx: &mut ViewContext<BaseKeymapSelector>) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
if let Some(selection) = self.matches.get(self.selected_index) {
let base_keymap = BaseKeymap::from_names(&selection.string);
update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
*setting = Some(base_keymap)
});
}
- cx.emit(PickerEvent::Dismiss);
+
+ self.view
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
}
- fn dismissed(&mut self, _cx: &mut ViewContext<BaseKeymapSelector>) {}
+ fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut gpui::MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> gpui::AnyElement<Picker<Self>> {
- let theme = &theme::current(cx);
+ _cx: &mut gpui::ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
let keymap_match = &self.matches[ix];
- let style = theme.picker.item.in_state(selected).style_for(mouse_state);
- Label::new(keymap_match.string.clone(), style.label.clone())
- .with_highlights(keymap_match.positions.clone())
- .contained()
- .with_style(style.container)
- .into_any()
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .selected(selected)
+ .child(HighlightedLabel::new(
+ keymap_match.string.clone(),
+ keymap_match.positions.clone(),
+ )),
+ )
}
}
@@ -1,6 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
pub enum BaseKeymap {
@@ -44,7 +44,7 @@ impl BaseKeymap {
}
}
-impl Setting for BaseKeymap {
+impl Settings for BaseKeymap {
const KEY: Option<&'static str> = Some("base_keymap");
type FileContent = Option<Self>;
@@ -52,7 +52,7 @@ impl Setting for BaseKeymap {
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,
@@ -1,19 +1,21 @@
mod base_keymap_picker;
mod base_keymap_setting;
-use crate::base_keymap_picker::ToggleBaseKeymapSelector;
use client::TelemetrySettings;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
- elements::{Flex, Label, ParentElement},
- AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
+ svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+ ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
};
-use settings::{update_settings_file, SettingsStore};
-use std::{borrow::Cow, sync::Arc};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use ui::{prelude::*, Checkbox};
use vim::VimModeSetting;
use workspace::{
- dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
- WorkspaceId,
+ dock::DockPosition,
+ item::{Item, ItemEvent},
+ open_new, AppState, Welcome, Workspace, WorkspaceId,
};
pub use base_keymap_setting::BaseKeymap;
@@ -21,22 +23,25 @@ pub use base_keymap_setting::BaseKeymap;
pub const FIRST_OPEN: &str = "first_open";
pub fn init(cx: &mut AppContext) {
- settings::register::<BaseKeymap>(cx);
+ BaseKeymap::register(cx);
- cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
- let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
- workspace.add_item(Box::new(welcome_page), cx)
- });
+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+ workspace.register_action(|workspace, _: &Welcome, cx| {
+ let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+ workspace.add_item(Box::new(welcome_page), cx)
+ });
+ })
+ .detach();
base_keymap_picker::init(cx);
}
-pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
+pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
- let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
+ let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
- cx.focus(&welcome_page);
+ cx.focus_view(&welcome_page);
cx.notify();
})
.detach();
@@ -47,227 +52,213 @@ pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
}
pub struct WelcomePage {
- workspace: WeakViewHandle<Workspace>,
+ workspace: WeakView<Workspace>,
+ focus_handle: FocusHandle,
_settings_subscription: Subscription,
}
-impl Entity for WelcomePage {
- type Event = ();
-}
-
-impl View for WelcomePage {
- fn ui_name() -> &'static str {
- "WelcomePage"
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
- let self_handle = cx.handle();
- let theme = theme::current(cx);
- let width = theme.welcome.page_width;
-
- let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
- let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
-
- enum Metrics {}
- enum Diagnostics {}
-
- PaneBackdrop::new(
- self_handle.id(),
- Flex::column()
- .with_child(
- Flex::column()
- .with_child(
- theme::ui::svg(&theme.welcome.logo)
- .aligned()
- .contained()
- .aligned(),
- )
- .with_child(
- Label::new(
- "Code at the speed of thought",
- theme.welcome.logo_subheading.text.clone(),
- )
- .aligned()
- .contained()
- .with_style(theme.welcome.logo_subheading.container),
- )
- .contained()
- .with_style(theme.welcome.heading_group)
- .constrained()
- .with_width(width),
+impl Render for WelcomePage {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
+ h_stack().full().track_focus(&self.focus_handle).child(
+ v_stack()
+ .w_96()
+ .gap_4()
+ .mx_auto()
+ .child(
+ svg()
+ .path("icons/logo_96.svg")
+ .text_color(gpui::white())
+ .w(px(96.))
+ .h(px(96.))
+ .mx_auto(),
)
- .with_child(
- Flex::column()
- .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
- "Choose a theme",
- width,
- &theme.welcome.button,
- cx,
- |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- theme_selector::toggle(workspace, &Default::default(), cx)
- })
- }
- },
- ))
- .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
- "Choose a keymap",
- width,
- &theme.welcome.button,
- cx,
- |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- base_keymap_picker::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- }
- },
- ))
- .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
- "Install the CLI",
- width,
- &theme.welcome.button,
- cx,
- |_, _, cx| {
- cx.app_context()
- .spawn(|cx| async move { install_cli::install_cli(&cx).await })
- .detach_and_log_err(cx);
- },
- ))
- .contained()
- .with_style(theme.welcome.button_group)
- .constrained()
- .with_width(width),
+ .child(
+ h_stack()
+ .justify_center()
+ .child(Label::new("Code at the speed of thought")),
)
- .with_child(
- Flex::column()
- .with_child(
- theme::ui::checkbox::<Diagnostics, Self, _>(
- "Enable vim mode",
- &theme.welcome.checkbox,
- vim_mode_setting,
- 0,
- cx,
- |this, checked, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let fs = workspace.read(cx).app_state().fs.clone();
- update_settings_file::<VimModeSetting>(
- fs,
- cx,
- move |setting| *setting = Some(checked),
- )
- }
- },
- )
- .contained()
- .with_style(theme.welcome.checkbox_container),
+ .child(
+ v_stack()
+ .gap_2()
+ .child(
+ Button::new("choose-theme", "Choose a theme")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ theme_selector::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
)
- .with_child(
- theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
- Flex::column()
- .with_child(
- Label::new(
- "Send anonymous usage data",
- theme.welcome.checkbox.label.text.clone(),
+ .child(
+ Button::new("choose-keymap", "Choose a keymap")
+ .full_width()
+ .on_click(cx.listener(|this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ base_keymap_picker::toggle(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ })),
+ )
+ .child(
+ Button::new("install-cli", "Install the CLI")
+ .full_width()
+ .on_click(cx.listener(|_, _, cx| {
+ cx.app_mut()
+ .spawn(
+ |cx| async move { install_cli::install_cli(&cx).await },
)
- .contained()
- .with_style(theme.welcome.checkbox.label.container),
+ .detach_and_log_err(cx);
+ })),
+ ),
+ )
+ .child(
+ v_stack()
+ .p_3()
+ .gap_2()
+ .bg(cx.theme().colors().elevated_surface_background)
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .rounded_md()
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-vim",
+ if VimModeSetting::get_global(cx).0 {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
)
- .with_child(
- Label::new(
- "Help > View Telemetry",
- theme.welcome.usage_note.text.clone(),
- )
- .contained()
- .with_style(theme.welcome.usage_note.container),
- ),
- &theme.welcome.checkbox,
- telemetry_settings.metrics,
- 0,
- cx,
- |this, checked, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let fs = workspace.read(cx).app_state().fs.clone();
- update_settings_file::<TelemetrySettings>(
- fs,
- cx,
- move |setting| setting.metrics = Some(checked),
- )
- }
- },
- )
- .contained()
- .with_style(theme.welcome.checkbox_container),
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.update_settings::<VimModeSetting>(
+ selection,
+ cx,
+ |setting, value| *setting = Some(value),
+ );
+ },
+ )),
+ )
+ .child(Label::new("Enable vim mode")),
)
- .with_child(
- theme::ui::checkbox::<Diagnostics, Self, _>(
- "Send crash reports",
- &theme.welcome.checkbox,
- telemetry_settings.diagnostics,
- 1,
- cx,
- |this, checked, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let fs = workspace.read(cx).app_state().fs.clone();
- update_settings_file::<TelemetrySettings>(
- fs,
- cx,
- move |setting| setting.diagnostics = Some(checked),
- )
- }
- },
- )
- .contained()
- .with_style(theme.welcome.checkbox_container),
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-telemetry",
+ if TelemetrySettings::get_global(cx).metrics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
+ )
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ |settings, value| settings.metrics = Some(value),
+ );
+ },
+ )),
+ )
+ .child(Label::new("Send anonymous usage data")),
)
- .contained()
- .with_style(theme.welcome.checkbox_group)
- .constrained()
- .with_width(width),
- )
- .constrained()
- .with_max_width(width)
- .contained()
- .with_uniform_padding(10.)
- .aligned()
- .into_any(),
+ .child(
+ h_stack()
+ .gap_2()
+ .child(
+ Checkbox::new(
+ "enable-crash",
+ if TelemetrySettings::get_global(cx).diagnostics {
+ ui::Selection::Selected
+ } else {
+ ui::Selection::Unselected
+ },
+ )
+ .on_click(cx.listener(
+ move |this, selection, cx| {
+ this.update_settings::<TelemetrySettings>(
+ selection,
+ cx,
+ |settings, value| {
+ settings.diagnostics = Some(value)
+ },
+ );
+ },
+ )),
+ )
+ .child(Label::new("Send crash reports")),
+ ),
+ ),
)
- .into_any_named("welcome page")
}
}
impl WelcomePage {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
WelcomePage {
+ focus_handle: cx.focus_handle(),
workspace: workspace.weak_handle(),
- _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
+ _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+ }
+ }
+
+ fn update_settings<T: Settings>(
+ &mut self,
+ selection: &Selection,
+ cx: &mut ViewContext<Self>,
+ callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
+ ) {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let fs = workspace.read(cx).app_state().fs.clone();
+ let selection = *selection;
+ settings::update_settings_file::<T>(fs, cx, move |settings| {
+ let value = match selection {
+ Selection::Unselected => false,
+ Selection::Selected => true,
+ _ => return,
+ };
+
+ callback(settings, value)
+ });
}
}
}
-impl Item for WelcomePage {
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
- Some("Welcome to Zed!".into())
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl FocusableView for WelcomePage {
+ fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
}
+}
- fn tab_content<T: 'static>(
- &self,
- _detail: Option<usize>,
- style: &theme::Tab,
- _cx: &gpui::AppContext,
- ) -> AnyElement<T> {
- Flex::row()
- .with_child(
- Label::new("Welcome to Zed!", style.label.clone())
- .aligned()
- .contained(),
- )
- .into_any()
+impl Item for WelcomePage {
+ type Event = ItemEvent;
+
+ fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
+ Label::new("Welcome to Zed!")
+ .color(if selected {
+ Color::Default
+ } else {
+ Color::Muted
+ })
+ .into_any_element()
}
fn show_toolbar(&self) -> bool {
@@ -278,10 +269,15 @@ impl Item for WelcomePage {
&self,
_workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
- ) -> Option<Self> {
- Some(WelcomePage {
+ ) -> Option<View<Self>> {
+ Some(cx.new_view(|cx| WelcomePage {
+ focus_handle: cx.focus_handle(),
workspace: self.workspace.clone(),
- _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
- })
+ _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+ }))
+ }
+
+ fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+ f(*event)
}
}
@@ -1,37 +0,0 @@
-[package]
-name = "welcome2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/welcome.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-client = { package = "client2", path = "../client2" }
-editor = { package = "editor2", path = "../editor2" }
-fs = { package = "fs2", path = "../fs2" }
-fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-ui = { package = "ui2", path = "../ui2" }
-db = { package = "db2", path = "../db2" }
-install_cli = { package = "install_cli2", path = "../install_cli2" }
-project = { package = "project2", path = "../project2" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
-util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-vim = { package = "vim2", path = "../vim2" }
-
-anyhow.workspace = true
-log.workspace = true
-schemars.workspace = true
-serde.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -1,208 +0,0 @@
-use super::base_keymap_setting::BaseKeymap;
-use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{
- actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
- ViewContext, VisualContext, WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use project::Fs;
-use settings::{update_settings_file, Settings};
-use std::sync::Arc;
-use ui::{prelude::*, ListItem, ListItemSpacing};
-use util::ResultExt;
-use workspace::{ui::HighlightedLabel, ModalView, Workspace};
-
-actions!(welcome, [ToggleBaseKeymapSelector]);
-
-pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|workspace: &mut Workspace, _cx| {
- workspace.register_action(toggle);
- })
- .detach();
-}
-
-pub fn toggle(
- workspace: &mut Workspace,
- _: &ToggleBaseKeymapSelector,
- cx: &mut ViewContext<Workspace>,
-) {
- let fs = workspace.app_state().fs.clone();
- workspace.toggle_modal(cx, |cx| {
- BaseKeymapSelector::new(
- BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
- cx,
- )
- });
-}
-
-pub struct BaseKeymapSelector {
- focus_handle: gpui::FocusHandle,
- picker: View<Picker<BaseKeymapSelectorDelegate>>,
-}
-
-impl FocusableView for BaseKeymapSelector {
- fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
-impl ModalView for BaseKeymapSelector {}
-
-impl BaseKeymapSelector {
- pub fn new(
- delegate: BaseKeymapSelectorDelegate,
- cx: &mut ViewContext<BaseKeymapSelector>,
- ) -> Self {
- let picker = cx.new_view(|cx| Picker::new(delegate, cx));
- let focus_handle = cx.focus_handle();
- Self {
- focus_handle,
- picker,
- }
- }
-}
-
-impl Render for BaseKeymapSelector {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- self.picker.clone()
- }
-}
-
-pub struct BaseKeymapSelectorDelegate {
- view: WeakView<BaseKeymapSelector>,
- matches: Vec<StringMatch>,
- selected_index: usize,
- fs: Arc<dyn Fs>,
-}
-
-impl BaseKeymapSelectorDelegate {
- fn new(
- weak_view: WeakView<BaseKeymapSelector>,
- fs: Arc<dyn Fs>,
- cx: &mut ViewContext<BaseKeymapSelector>,
- ) -> Self {
- let base = BaseKeymap::get(None, cx);
- let selected_index = BaseKeymap::OPTIONS
- .iter()
- .position(|(_, value)| value == base)
- .unwrap_or(0);
- Self {
- view: weak_view,
- matches: Vec::new(),
- selected_index,
- fs,
- }
- }
-}
-
-impl PickerDelegate for BaseKeymapSelectorDelegate {
- type ListItem = ui::ListItem;
-
- fn placeholder_text(&self) -> Arc<str> {
- "Select a base keymap...".into()
- }
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn selected_index(&self) -> usize {
- self.selected_index
- }
-
- fn set_selected_index(
- &mut self,
- ix: usize,
- _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
- ) {
- self.selected_index = ix;
- }
-
- fn update_matches(
- &mut self,
- query: String,
- cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
- ) -> Task<()> {
- let background = cx.background_executor().clone();
- let candidates = BaseKeymap::names()
- .enumerate()
- .map(|(id, name)| StringMatchCandidate {
- id,
- char_bag: name.into(),
- string: name.into(),
- })
- .collect::<Vec<_>>();
-
- cx.spawn(|this, mut cx| async move {
- let matches = if query.is_empty() {
- candidates
- .into_iter()
- .enumerate()
- .map(|(index, candidate)| StringMatch {
- candidate_id: index,
- string: candidate.string,
- positions: Vec::new(),
- score: 0.0,
- })
- .collect()
- } else {
- match_strings(
- &candidates,
- &query,
- false,
- 100,
- &Default::default(),
- background,
- )
- .await
- };
-
- this.update(&mut cx, |this, _| {
- this.delegate.matches = matches;
- this.delegate.selected_index = this
- .delegate
- .selected_index
- .min(this.delegate.matches.len().saturating_sub(1));
- })
- .log_err();
- })
- }
-
- fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
- if let Some(selection) = self.matches.get(self.selected_index) {
- let base_keymap = BaseKeymap::from_names(&selection.string);
- update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
- *setting = Some(base_keymap)
- });
- }
-
- self.view
- .update(cx, |_, cx| {
- cx.emit(DismissEvent);
- })
- .ok();
- }
-
- fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _cx: &mut gpui::ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- let keymap_match = &self.matches[ix];
-
- Some(
- ListItem::new(ix)
- .inset(true)
- .spacing(ListItemSpacing::Sparse)
- .selected(selected)
- .child(HighlightedLabel::new(
- keymap_match.string.clone(),
- keymap_match.positions.clone(),
- )),
- )
- }
-}
@@ -1,65 +0,0 @@
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
-pub enum BaseKeymap {
- #[default]
- VSCode,
- JetBrains,
- SublimeText,
- Atom,
- TextMate,
-}
-
-impl BaseKeymap {
- pub const OPTIONS: [(&'static str, Self); 5] = [
- ("VSCode (Default)", Self::VSCode),
- ("Atom", Self::Atom),
- ("JetBrains", Self::JetBrains),
- ("Sublime Text", Self::SublimeText),
- ("TextMate", Self::TextMate),
- ];
-
- pub fn asset_path(&self) -> Option<&'static str> {
- match self {
- BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
- BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
- BaseKeymap::Atom => Some("keymaps/atom.json"),
- BaseKeymap::TextMate => Some("keymaps/textmate.json"),
- BaseKeymap::VSCode => None,
- }
- }
-
- pub fn names() -> impl Iterator<Item = &'static str> {
- Self::OPTIONS.iter().map(|(name, _)| *name)
- }
-
- pub fn from_names(option: &str) -> BaseKeymap {
- Self::OPTIONS
- .iter()
- .copied()
- .find_map(|(name, value)| (name == option).then(|| value))
- .unwrap_or_default()
- }
-}
-
-impl Settings for BaseKeymap {
- const KEY: Option<&'static str> = Some("base_keymap");
-
- type FileContent = Option<Self>;
-
- fn load(
- default_value: &Self::FileContent,
- user_values: &[&Self::FileContent],
- _: &mut gpui::AppContext,
- ) -> anyhow::Result<Self>
- where
- Self: Sized,
- {
- Ok(user_values
- .first()
- .and_then(|v| **v)
- .unwrap_or(default_value.unwrap()))
- }
-}
@@ -1,283 +0,0 @@
-mod base_keymap_picker;
-mod base_keymap_setting;
-
-use client::TelemetrySettings;
-use db::kvp::KEY_VALUE_STORE;
-use gpui::{
- svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
- ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
- WindowContext,
-};
-use settings::{Settings, SettingsStore};
-use std::sync::Arc;
-use ui::{prelude::*, Checkbox};
-use vim::VimModeSetting;
-use workspace::{
- dock::DockPosition,
- item::{Item, ItemEvent},
- open_new, AppState, Welcome, Workspace, WorkspaceId,
-};
-
-pub use base_keymap_setting::BaseKeymap;
-
-pub const FIRST_OPEN: &str = "first_open";
-
-pub fn init(cx: &mut AppContext) {
- BaseKeymap::register(cx);
-
- cx.observe_new_views(|workspace: &mut Workspace, _cx| {
- workspace.register_action(|workspace, _: &Welcome, cx| {
- let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
- workspace.add_item(Box::new(welcome_page), cx)
- });
- })
- .detach();
-
- base_keymap_picker::init(cx);
-}
-
-pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
- open_new(&app_state, cx, |workspace, cx| {
- workspace.toggle_dock(DockPosition::Left, cx);
- let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
- workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
- cx.focus_view(&welcome_page);
- cx.notify();
- })
- .detach();
-
- db::write_and_log(cx, || {
- KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
- });
-}
-
-pub struct WelcomePage {
- workspace: WeakView<Workspace>,
- focus_handle: FocusHandle,
- _settings_subscription: Subscription,
-}
-
-impl Render for WelcomePage {
- fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
- h_stack().full().track_focus(&self.focus_handle).child(
- v_stack()
- .w_96()
- .gap_4()
- .mx_auto()
- .child(
- svg()
- .path("icons/logo_96.svg")
- .text_color(gpui::white())
- .w(px(96.))
- .h(px(96.))
- .mx_auto(),
- )
- .child(
- h_stack()
- .justify_center()
- .child(Label::new("Code at the speed of thought")),
- )
- .child(
- v_stack()
- .gap_2()
- .child(
- Button::new("choose-theme", "Choose a theme")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- theme_selector::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("choose-keymap", "Choose a keymap")
- .full_width()
- .on_click(cx.listener(|this, _, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- base_keymap_picker::toggle(
- workspace,
- &Default::default(),
- cx,
- )
- })
- .ok();
- })),
- )
- .child(
- Button::new("install-cli", "Install the CLI")
- .full_width()
- .on_click(cx.listener(|_, _, cx| {
- cx.app_mut()
- .spawn(
- |cx| async move { install_cli::install_cli(&cx).await },
- )
- .detach_and_log_err(cx);
- })),
- ),
- )
- .child(
- v_stack()
- .p_3()
- .gap_2()
- .bg(cx.theme().colors().elevated_surface_background)
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_md()
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-vim",
- if VimModeSetting::get_global(cx).0 {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.update_settings::<VimModeSetting>(
- selection,
- cx,
- |setting, value| *setting = Some(value),
- );
- },
- )),
- )
- .child(Label::new("Enable vim mode")),
- )
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-telemetry",
- if TelemetrySettings::get_global(cx).metrics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- |settings, value| settings.metrics = Some(value),
- );
- },
- )),
- )
- .child(Label::new("Send anonymous usage data")),
- )
- .child(
- h_stack()
- .gap_2()
- .child(
- Checkbox::new(
- "enable-crash",
- if TelemetrySettings::get_global(cx).diagnostics {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- )
- .on_click(cx.listener(
- move |this, selection, cx| {
- this.update_settings::<TelemetrySettings>(
- selection,
- cx,
- |settings, value| {
- settings.diagnostics = Some(value)
- },
- );
- },
- )),
- )
- .child(Label::new("Send crash reports")),
- ),
- ),
- )
- }
-}
-
-impl WelcomePage {
- pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
- WelcomePage {
- focus_handle: cx.focus_handle(),
- workspace: workspace.weak_handle(),
- _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
- }
- }
-
- fn update_settings<T: Settings>(
- &mut self,
- selection: &Selection,
- cx: &mut ViewContext<Self>,
- callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
- ) {
- if let Some(workspace) = self.workspace.upgrade() {
- let fs = workspace.read(cx).app_state().fs.clone();
- let selection = *selection;
- settings::update_settings_file::<T>(fs, cx, move |settings| {
- let value = match selection {
- Selection::Unselected => false,
- Selection::Selected => true,
- _ => return,
- };
-
- callback(settings, value)
- });
- }
- }
-}
-
-impl EventEmitter<ItemEvent> for WelcomePage {}
-
-impl FocusableView for WelcomePage {
- fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Item for WelcomePage {
- type Event = ItemEvent;
-
- fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
- Label::new("Welcome to Zed!")
- .color(if selected {
- Color::Default
- } else {
- Color::Muted
- })
- .into_any_element()
- }
-
- fn show_toolbar(&self) -> bool {
- false
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<Self>> {
- Some(cx.new_view(|cx| WelcomePage {
- focus_handle: cx.focus_handle(),
- workspace: self.workspace.clone(),
- _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
- }))
- }
-
- fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
- f(*event)
- }
-}
@@ -23,7 +23,7 @@ breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
call = { package = "call2", path = "../call2" }
channel = { package = "channel2", path = "../channel2" }
cli = { path = "../cli" }
-collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
+collab_ui = { path = "../collab_ui" }
collections = { path = "../collections" }
command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
@@ -56,7 +56,7 @@ outline = { package = "outline2", path = "../outline2" }
project = { package = "project2", path = "../project2" }
project_panel = { package = "project_panel2", path = "../project_panel2" }
project_symbols = { package = "project_symbols2", path = "../project_symbols2" }
-quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
+quick_action_bar = { path = "../quick_action_bar" }
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
rope = { package = "rope2", path = "../rope2"}
rpc = { package = "rpc2", path = "../rpc2" }
@@ -72,7 +72,7 @@ util = { path = "../util" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
vim = { package = "vim2", path = "../vim2" }
workspace = { package = "workspace2", path = "../workspace2" }
-welcome = { package = "welcome2", path = "../welcome2" }
+welcome = { path = "../welcome" }
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
anyhow.workspace = true
async-compression.workspace = true