Cargo.lock 🔗
@@ -935,6 +935,7 @@ dependencies = [
"fuzzy",
"gpui",
"log",
+ "picker",
"postage",
"serde",
"settings",
Antonio Scandurra and Nathan Sobo created
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Cargo.lock | 1
assets/themes/cave-dark.json | 33 --
assets/themes/cave-light.json | 33 --
assets/themes/dark.json | 33 --
assets/themes/light.json | 33 --
assets/themes/solarized-dark.json | 33 --
assets/themes/solarized-light.json | 33 --
assets/themes/sulphurpool-dark.json | 33 --
assets/themes/sulphurpool-light.json | 33 --
crates/contacts_panel/Cargo.toml | 1
crates/contacts_panel/src/contact_finder.rs | 254 +++++++++++-----------
crates/contacts_panel/src/contacts_panel.rs | 20 -
crates/theme/src/theme.rs | 7
styles/src/styleTree/contactFinder.ts | 19 -
14 files changed, 140 insertions(+), 426 deletions(-)
@@ -935,6 +935,7 @@ dependencies = [
"fuzzy",
"gpui",
"log",
+ "picker",
"postage",
"serde",
"settings",
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#19171c",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#e2dfe7",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#7e7887",
- "size": 14
- },
- "selection": {
- "cursor": "#576ddb",
- "selection": "#576ddb3d"
- },
- "border": {
- "color": "#26232a",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#e2dfe7",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#efecf4",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#26232a",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#655f6d",
- "size": 14
- },
- "selection": {
- "cursor": "#576ddb",
- "selection": "#576ddb3d"
- },
- "border": {
- "color": "#e2dfe7",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#26232a",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#000000",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#f1f1f1",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#474747",
- "size": 14
- },
- "selection": {
- "cursor": "#2472f2",
- "selection": "#2472f23d"
- },
- "border": {
- "color": "#232323",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#f1f1f1",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#ffffff",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#2b2b2b",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#808080",
- "size": 14
- },
- "selection": {
- "cursor": "#2472f2",
- "selection": "#2472f23d"
- },
- "border": {
- "color": "#d5d5d5",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#2b2b2b",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#002b36",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#eee8d5",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#839496",
- "size": 14
- },
- "selection": {
- "cursor": "#268bd2",
- "selection": "#268bd23d"
- },
- "border": {
- "color": "#073642",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#eee8d5",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#fdf6e3",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#073642",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#657b83",
- "size": 14
- },
- "selection": {
- "cursor": "#268bd2",
- "selection": "#268bd23d"
- },
- "border": {
- "color": "#eee8d5",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#073642",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#202746",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#dfe2f1",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#898ea4",
- "size": 14
- },
- "selection": {
- "cursor": "#3d8fd1",
- "selection": "#3d8fd13d"
- },
- "border": {
- "color": "#293256",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#dfe2f1",
- "size": 14,
"padding": {
"left": 8
}
@@ -1466,45 +1466,12 @@
2
]
},
- "max_width": 540,
- "max_height": 420,
- "query_editor": {
- "background": "#f5f7ff",
- "corner_radius": 6,
- "text": {
- "family": "Zed Mono",
- "color": "#293256",
- "size": 14
- },
- "placeholder_text": {
- "family": "Zed Mono",
- "color": "#6b7394",
- "size": 14
- },
- "selection": {
- "cursor": "#3d8fd1",
- "selection": "#3d8fd13d"
- },
- "border": {
- "color": "#dfe2f1",
- "width": 1
- },
- "padding": {
- "bottom": 4,
- "left": 8,
- "right": 8,
- "top": 4
- }
- },
"row_height": 28,
"contact_avatar": {
"corner_radius": 10,
"width": 18
},
"contact_username": {
- "family": "Zed Mono",
- "color": "#293256",
- "size": 14,
"padding": {
"left": 8
}
@@ -12,6 +12,7 @@ client = { path = "../client" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
+picker = { path = "../picker" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
@@ -1,25 +1,34 @@
use client::{ContactRequestStatus, User, UserStore};
-use editor::Editor;
use gpui::{
- color::Color, elements::*, platform::CursorStyle, Entity, LayoutContext, ModelHandle,
- RenderContext, Task, View, ViewContext, ViewHandle,
+ actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
+ ViewContext, ViewHandle,
};
+use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
+use workspace::Workspace;
-use crate::{RemoveContact, RequestContact};
+actions!(contact_finder, [Toggle]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ Picker::<ContactFinder>::init(cx);
+ cx.add_action(ContactFinder::toggle);
+}
pub struct ContactFinder {
- query_editor: ViewHandle<Editor>,
- list_state: UniformListState,
+ picker: ViewHandle<Picker<Self>>,
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
- contacts_search_task: Option<Task<Option<()>>>,
+ selected_index: usize,
+}
+
+pub enum Event {
+ Dismissed,
}
impl Entity for ContactFinder {
- type Event = ();
+ type Event = Event;
}
impl View for ContactFinder {
@@ -27,149 +36,150 @@ impl View for ContactFinder {
"ContactFinder"
}
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = cx.global::<Settings>().theme.clone();
- let user_store = self.user_store.clone();
- let potential_contacts = self.potential_contacts.clone();
- Flex::column()
- .with_child(
- ChildView::new(self.query_editor.clone())
- .contained()
- .with_style(theme.contact_finder.query_editor.container)
- .boxed(),
- )
- .with_child(
- UniformList::new(self.list_state.clone(), self.potential_contacts.len(), {
- let theme = theme.clone();
- move |range, items, cx| {
- items.extend(range.map(|ix| {
- Self::render_potential_contact(
- &potential_contacts[ix],
- &user_store,
- &theme.contact_finder,
- cx,
- )
- }))
- }
- })
- .flex(1., false)
- .boxed(),
- )
- .contained()
- .with_style(theme.contact_finder.container)
- .constrained()
- .with_max_width(theme.contact_finder.max_width)
- .with_max_height(theme.contact_finder.max_height)
- .boxed()
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ cx.focus(&self.picker);
}
}
-impl ContactFinder {
- pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
- let query_editor = cx.add_view(|cx| {
- Editor::single_line(Some(|theme| theme.contact_finder.query_editor.clone()), cx)
- });
+impl PickerDelegate for ContactFinder {
+ fn match_count(&self) -> usize {
+ self.potential_contacts.len()
+ }
- cx.subscribe(&query_editor, |this, _, event, cx| {
- if let editor::Event::BufferEdited = event {
- this.query_changed(cx)
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+ let search_users = self
+ .user_store
+ .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+
+ cx.spawn(|this, mut cx| async move {
+ async {
+ let potential_contacts = search_users.await?;
+ this.update(&mut cx, |this, cx| {
+ this.potential_contacts = potential_contacts.into();
+ cx.notify();
+ });
+ Ok(())
}
+ .log_err()
+ .await;
})
- .detach();
- Self {
- query_editor,
- list_state: Default::default(),
- potential_contacts: Arc::from([]),
- user_store,
- contacts_search_task: None,
- }
}
- fn render_potential_contact(
- contact: &User,
- user_store: &ModelHandle<UserStore>,
- theme: &theme::ContactFinder,
- cx: &mut LayoutContext,
- ) -> ElementBox {
- enum RequestContactButton {}
+ fn confirm(&mut self, cx: &mut ViewContext<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();
+ }
+ _ => {}
+ }
+ }
+ }
- let contact_id = contact.id;
- let request_status = user_store.read(cx).contact_request_status(&contact);
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
+ }
+ fn render_match(
+ &self,
+ ix: usize,
+ mouse_state: &MouseState,
+ selected: bool,
+ cx: &gpui::AppContext,
+ ) -> ElementBox {
+ let theme = &cx.global::<Settings>().theme;
+ let contact = &self.potential_contacts[ix];
+ let request_status = self.user_store.read(cx).contact_request_status(&contact);
+ let label = match request_status {
+ ContactRequestStatus::None | ContactRequestStatus::RequestReceived => "+",
+ ContactRequestStatus::RequestSent => "-",
+ ContactRequestStatus::Pending | ContactRequestStatus::RequestAccepted => "…",
+ };
+ let style = theme.picker.item.style_for(mouse_state, selected);
Flex::row()
.with_children(contact.avatar.clone().map(|avatar| {
Image::new(avatar)
- .with_style(theme.contact_avatar)
+ .with_style(theme.contact_finder.contact_avatar)
.aligned()
.left()
.boxed()
}))
+ .with_child(
+ Label::new(contact.github_login.clone(), style.label.clone())
+ .contained()
+ .with_style(theme.contact_finder.contact_username)
+ .aligned()
+ .left()
+ .boxed(),
+ )
.with_child(
Label::new(
- contact.github_login.clone(),
- theme.contact_username.text.clone(),
+ label.to_string(),
+ theme.contact_finder.contact_button.text.clone(),
)
.contained()
- .with_style(theme.contact_username.container)
+ .with_style(theme.contact_finder.contact_button.container)
.aligned()
- .left()
- .boxed(),
- )
- .with_child(
- MouseEventHandler::new::<RequestContactButton, _, _>(
- contact.id as usize,
- cx,
- |_, _| {
- let label = match request_status {
- ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
- "+"
- }
- ContactRequestStatus::RequestSent => "-",
- ContactRequestStatus::Pending
- | ContactRequestStatus::RequestAccepted => "…",
- };
-
- Label::new(label.to_string(), theme.contact_button.text.clone())
- .contained()
- .with_style(theme.contact_button.container)
- .aligned()
- .flex_float()
- .boxed()
- },
- )
- .on_click(move |_, cx| match request_status {
- ContactRequestStatus::None => {
- cx.dispatch_action(RequestContact(contact_id));
- }
- ContactRequestStatus::RequestSent => {
- cx.dispatch_action(RemoveContact(contact_id));
- }
- _ => {}
- })
- .with_cursor_style(CursorStyle::PointingHand)
+ .flex_float()
.boxed(),
)
+ .contained()
+ .with_style(style.container)
.constrained()
- .with_height(theme.row_height)
+ .with_height(theme.contact_finder.row_height)
.boxed()
}
+}
- fn query_changed(&mut self, cx: &mut ViewContext<Self>) {
- let query = self.query_editor.read(cx).text(cx);
- let search_users = self
- .user_store
- .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
+impl ContactFinder {
+ fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ workspace.toggle_modal(cx, |cx, workspace| {
+ let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
+ cx.subscribe(&finder, Self::on_event).detach();
+ finder
+ });
+ }
- self.contacts_search_task = Some(cx.spawn(|this, mut cx| {
- async move {
- let potential_contacts = search_users.await?;
- this.update(&mut cx, |this, cx| {
- this.potential_contacts = potential_contacts.into();
- cx.notify();
- });
- Ok(())
+ pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+ let this = cx.weak_handle();
+ Self {
+ picker: cx.add_view(|cx| Picker::new(this, cx)),
+ potential_contacts: Arc::from([]),
+ user_store,
+ selected_index: 0,
+ }
+ }
+
+ fn on_event(
+ workspace: &mut Workspace,
+ _: ViewHandle<Self>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ match event {
+ Event::Dismissed => {
+ workspace.dismiss_modal(cx);
}
- .log_err()
- }));
+ }
}
}
@@ -1,11 +1,9 @@
mod contact_finder;
use client::{Contact, ContactRequestStatus, User, UserStore};
-use contact_finder::ContactFinder;
use editor::Editor;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions,
@@ -16,9 +14,8 @@ use gpui::{
use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
-use workspace::{AppState, JoinProject, Workspace};
+use workspace::{AppState, JoinProject};
-actions!(contacts_panel, [FindNewContacts]);
impl_actions!(
contacts_panel,
[RequestContact, RemoveContact, RespondToContactRequest]
@@ -54,10 +51,10 @@ pub struct RespondToContactRequest {
}
pub fn init(cx: &mut MutableAppContext) {
+ contact_finder::init(cx);
cx.add_action(ContactsPanel::request_contact);
cx.add_action(ContactsPanel::remove_contact);
cx.add_action(ContactsPanel::respond_to_contact_request);
- cx.add_action(ContactsPanel::find_new_contacts);
}
impl ContactsPanel {
@@ -588,16 +585,6 @@ impl ContactsPanel {
})
.detach();
}
-
- fn find_new_contacts(
- workspace: &mut Workspace,
- _: &FindNewContacts,
- cx: &mut ViewContext<Workspace>,
- ) {
- workspace.toggle_modal(cx, |cx, workspace| {
- cx.add_view(|cx| ContactFinder::new(workspace.user_store().clone(), cx))
- });
- }
}
pub enum Event {}
@@ -638,7 +625,8 @@ impl View for ContactsPanel {
.aligned()
.boxed()
})
- .on_click(|_, cx| cx.dispatch_action(FindNewContacts))
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle))
.boxed(),
)
.constrained()
@@ -252,14 +252,9 @@ pub struct ContactsPanel {
#[derive(Deserialize, Default)]
pub struct ContactFinder {
- #[serde(flatten)]
- pub container: ContainerStyle,
- pub max_width: f32,
- pub max_height: f32,
- pub query_editor: FieldEditor,
pub row_height: f32,
pub contact_avatar: ImageStyle,
- pub contact_username: ContainedText,
+ pub contact_username: ContainerStyle,
pub contact_button: ContainedText,
}
@@ -1,33 +1,16 @@
import Theme from "../themes/theme";
import picker from "./picker";
-import { backgroundColor, border, player, text } from "./components";
+import { backgroundColor, text } from "./components";
export default function contactFinder(theme: Theme) {
return {
...picker(theme),
- maxWidth: 540.,
- maxHeight: 420.,
- queryEditor: {
- background: backgroundColor(theme, 500),
- cornerRadius: 6,
- text: text(theme, "mono", "primary"),
- placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
- selection: player(theme, 1).selection,
- border: border(theme, "secondary"),
- padding: {
- bottom: 4,
- left: 8,
- right: 8,
- top: 4,
- },
- },
rowHeight: 28,
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactUsername: {
- ...text(theme, "mono", "primary", { size: "sm" }),
padding: {
left: 8,
},