assets/themes/cave-dark.json 🔗
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#8b8792",
Max Brunsfeld created
Also, allow joining projects using the keyboard.
assets/themes/cave-dark.json | 1
assets/themes/cave-light.json | 1
assets/themes/dark.json | 1
assets/themes/light.json | 1
assets/themes/solarized-dark.json | 1
assets/themes/solarized-light.json | 1
assets/themes/sulphurpool-dark.json | 1
assets/themes/sulphurpool-light.json | 1
crates/contacts_panel/src/contacts_panel.rs | 220 ++++++++++++++++------
crates/theme/src/theme.rs | 1
styles/src/styleTree/contactsPanel.ts | 1
11 files changed, 167 insertions(+), 63 deletions(-)
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#8b8792",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#585260",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#9c9c9c",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#474747",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#93a1a1",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#586e75",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#979db4",
@@ -1251,6 +1251,7 @@
"icon_width": 8
},
"row_height": 28,
+ "section_icon_size": 8,
"header_row": {
"family": "Zed Mono",
"color": "#5e6687",
@@ -15,7 +15,7 @@ use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use theme::IconButton;
-use workspace::menu::{SelectNext, SelectPrev};
+use workspace::menu::{Confirm, SelectNext, SelectPrev};
use workspace::{AppState, JoinProject};
impl_actions!(
@@ -23,9 +23,16 @@ impl_actions!(
[RequestContact, RemoveContact, RespondToContactRequest]
);
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+ Requests,
+ Online,
+ Offline,
+}
+
#[derive(Clone, Debug)]
enum ContactEntry {
- Header(&'static str),
+ Header(Section),
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
@@ -38,7 +45,9 @@ pub struct ContactsPanel {
list_state: ListState,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
+ collapsed_sections: Vec<Section>,
selection: Option<usize>,
+ app_state: Arc<AppState>,
_maintain_contacts: Subscription,
}
@@ -62,6 +71,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPanel::clear_filter);
cx.add_action(ContactsPanel::select_next);
cx.add_action(ContactsPanel::select_prev);
+ cx.add_action(ContactsPanel::confirm);
}
impl ContactsPanel {
@@ -97,18 +107,9 @@ impl ContactsPanel {
let is_selected = this.selection == Some(ix);
match &this.entries[ix] {
- ContactEntry::Header(text) => {
- let header_style =
- theme.header_row.style_for(&Default::default(), is_selected);
- Label::new(text.to_string(), header_style.text.clone())
- .contained()
- .aligned()
- .left()
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(header_style.container)
- .boxed()
+ ContactEntry::Header(section) => {
+ let is_collapsed = this.collapsed_sections.contains(§ion);
+ Self::render_header(*section, theme, is_selected, is_collapsed)
}
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(),
@@ -153,17 +154,64 @@ impl ContactsPanel {
}
}),
selection: None,
+ collapsed_sections: Default::default(),
entries: Default::default(),
match_candidates: Default::default(),
filter_editor: user_query_editor,
_maintain_contacts: cx
.observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
user_store: app_state.user_store.clone(),
+ app_state,
};
this.update_entries(cx);
this
}
+ fn render_header(
+ section: Section,
+ theme: &theme::ContactsPanel,
+ is_selected: bool,
+ is_collapsed: bool,
+ ) -> ElementBox {
+ let header_style = theme.header_row.style_for(&Default::default(), is_selected);
+ let text = match section {
+ Section::Requests => "Requests",
+ Section::Online => "Online",
+ Section::Offline => "Offline",
+ };
+ let icon_size = theme.section_icon_size;
+ Flex::row()
+ .with_child(
+ Svg::new(if is_collapsed {
+ "icons/disclosure-closed.svg"
+ } else {
+ "icons/disclosure-open.svg"
+ })
+ .with_color(header_style.text.color)
+ .constrained()
+ .with_max_width(icon_size)
+ .with_max_height(icon_size)
+ .aligned()
+ .constrained()
+ .with_width(icon_size)
+ .boxed(),
+ )
+ .with_child(
+ Label::new(text.to_string(), header_style.text.clone())
+ .aligned()
+ .left()
+ .contained()
+ .with_margin_left(theme.contact_username.container.margin.left)
+ .flex(1., true)
+ .boxed(),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(header_style.container)
+ .boxed()
+ }
+
fn render_contact(
contact: Arc<Contact>,
theme: &theme::ContactsPanel,
@@ -507,8 +555,10 @@ impl ContactsPanel {
}
if !request_entries.is_empty() {
- self.entries.push(ContactEntry::Header("Requests"));
- self.entries.append(&mut request_entries);
+ self.entries.push(ContactEntry::Header(Section::Requests));
+ if !self.collapsed_sections.contains(&Section::Requests) {
+ self.entries.append(&mut request_entries);
+ }
}
let contacts = user_store.contacts();
@@ -538,22 +588,27 @@ impl ContactsPanel {
.iter()
.partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
- for (matches, name) in [(online_contacts, "Online"), (offline_contacts, "Offline")] {
+ for (matches, section) in [
+ (online_contacts, Section::Online),
+ (offline_contacts, Section::Offline),
+ ] {
if !matches.is_empty() {
- self.entries.push(ContactEntry::Header(name));
- for mat in matches {
- let contact = &contacts[mat.candidate_id];
- self.entries.push(ContactEntry::Contact(contact.clone()));
- self.entries
- .extend(contact.projects.iter().enumerate().filter_map(
- |(ix, project)| {
- if project.worktree_root_names.is_empty() {
- None
- } else {
- Some(ContactEntry::ContactProject(contact.clone(), ix))
- }
- },
- ));
+ self.entries.push(ContactEntry::Header(section));
+ if !self.collapsed_sections.contains(§ion) {
+ for mat in matches {
+ let contact = &contacts[mat.candidate_id];
+ self.entries.push(ContactEntry::Contact(contact.clone()));
+ self.entries
+ .extend(contact.projects.iter().enumerate().filter_map(
+ |(ix, project)| {
+ if project.worktree_root_names.is_empty() {
+ None
+ } else {
+ Some(ContactEntry::ContactProject(contact.clone(), ix))
+ }
+ },
+ ));
+ }
}
}
}
@@ -624,6 +679,32 @@ impl ContactsPanel {
cx.notify();
self.list_state.reset(self.entries.len());
}
+
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ if let Some(selection) = self.selection {
+ if let Some(entry) = self.entries.get(selection) {
+ match entry {
+ ContactEntry::Header(section) => {
+ 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(cx);
+ }
+ ContactEntry::ContactProject(contact, project_ix) => {
+ cx.dispatch_global_action(JoinProject {
+ project_id: contact.projects[*project_ix].id,
+ app_state: self.app_state.clone(),
+ })
+ }
+ _ => {}
+ }
+ }
+ } else {
+ }
+ }
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
@@ -706,9 +787,9 @@ impl View for ContactsPanel {
impl PartialEq for ContactEntry {
fn eq(&self, other: &Self) -> bool {
match self {
- ContactEntry::Header(name_1) => {
- if let ContactEntry::Header(name_2) = other {
- return name_1 == name_2;
+ ContactEntry::Header(section_1) => {
+ if let ContactEntry::Header(section_2) = other {
+ return section_1 == section_2;
}
}
ContactEntry::IncomingRequest(user_1) => {
@@ -816,7 +897,7 @@ mod tests {
&[
"+",
"v Requests",
- " incoming user_one <=== selected",
+ " incoming user_one",
" outgoing user_two",
"v Online",
" user_four",
@@ -838,10 +919,10 @@ mod tests {
render_to_strings(&panel, cx),
&[
"+",
- "Online",
- " user_four <=== selected",
+ "v Online",
+ " user_four",
" dir2",
- "Offline",
+ "v Offline",
" user_five",
]
);
@@ -853,10 +934,10 @@ mod tests {
render_to_strings(&panel, cx),
&[
"+",
- "Online",
+ "v Online <=== selected",
" user_four",
- " dir2 <=== selected",
- "Offline",
+ " dir2",
+ "v Offline",
" user_five",
]
);
@@ -868,11 +949,11 @@ mod tests {
render_to_strings(&panel, cx),
&[
"+",
- "Online",
- " user_four",
+ "v Online",
+ " user_four <=== selected",
" dir2",
- "Offline",
- " user_five <=== selected",
+ "v Offline",
+ " user_five",
]
);
}
@@ -881,25 +962,38 @@ mod tests {
panel.read_with(cx, |panel, _| {
let mut entries = Vec::new();
entries.push("+".to_string());
- entries.extend(panel.entries.iter().map(|entry| match entry {
- ContactEntry::Header(name) => {
- format!("{}", name)
- }
- ContactEntry::IncomingRequest(user) => {
- format!(" incoming {}", user.github_login)
- }
- ContactEntry::OutgoingRequest(user) => {
- format!(" outgoing {}", user.github_login)
- }
- ContactEntry::Contact(contact) => {
- format!(" {}", contact.user.github_login)
- }
- ContactEntry::ContactProject(contact, project_ix) => {
- format!(
- " {}",
- contact.projects[*project_ix].worktree_root_names.join(", ")
- )
+ entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| {
+ let mut string = match entry {
+ ContactEntry::Header(name) => {
+ let icon = if panel.collapsed_sections.contains(name) {
+ ">"
+ } else {
+ "v"
+ };
+ format!("{} {:?}", icon, name)
+ }
+ ContactEntry::IncomingRequest(user) => {
+ format!(" incoming {}", user.github_login)
+ }
+ ContactEntry::OutgoingRequest(user) => {
+ format!(" outgoing {}", user.github_login)
+ }
+ ContactEntry::Contact(contact) => {
+ format!(" {}", contact.user.github_login)
+ }
+ ContactEntry::ContactProject(contact, project_ix) => {
+ format!(
+ " {}",
+ contact.projects[*project_ix].worktree_root_names.join(", ")
+ )
+ }
+ };
+
+ if panel.selection == Some(ix) {
+ string.push_str(" <=== selected");
}
+
+ string
}));
entries
})
@@ -249,6 +249,7 @@ pub struct ContactsPanel {
pub contact_button_spacing: f32,
pub disabled_contact_button: IconButton,
pub tree_branch: Interactive<TreeBranch>,
+ pub section_icon_size: f32,
}
#[derive(Deserialize, Default, Clone, Copy)]
@@ -68,6 +68,7 @@ export default function contactsPanel(theme: Theme) {
iconWidth: 8,
},
rowHeight: 28,
+ sectionIconSize: 8,
headerRow: {
...text(theme, "mono", "secondary", { size: "sm" }),
margin: { top: 14 },