Structure the contact finder more similarly to the channel modal

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2023-08-14 11:36:49 -07:00
parent b6f3dd51a0
commit 2bb9f7929d
10 changed files with 351 additions and 232 deletions

View file

@ -7,7 +7,7 @@ use call::ActiveCall;
use client::{ use client::{
proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
}; };
use contact_finder::build_contact_finder;
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
@ -46,6 +46,8 @@ use workspace::{
use crate::face_pile::FacePile; use crate::face_pile::FacePile;
use channel_modal::ChannelModal; use channel_modal::ChannelModal;
use self::contact_finder::ContactFinder;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RemoveChannel { struct RemoveChannel {
channel_id: u64, channel_id: u64,
@ -1945,7 +1947,7 @@ impl CollabPanel {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| { workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| { cx.add_view(|cx| {
let finder = build_contact_finder(self.user_store.clone(), cx); let mut finder = ContactFinder::new(self.user_store.clone(), cx);
finder.set_query(self.filter_editor.read(cx).text(cx), cx); finder.set_query(self.filter_editor.read(cx).text(cx), cx);
finder finder
}) })

View file

@ -66,7 +66,7 @@ impl ChannelModal {
}, },
cx, cx,
) )
.with_theme(|theme| theme.collab_panel.channel_modal.picker.clone()) .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
}); });
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
@ -143,7 +143,7 @@ impl View for ChannelModal {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.channel_modal; let theme = &theme::current(cx).collab_panel.tabbed_modal;
let mode = self.picker.read(cx).delegate().mode; let mode = self.picker.read(cx).delegate().mode;
let Some(channel) = self let Some(channel) = self
@ -160,12 +160,12 @@ impl View for ChannelModal {
mode: Mode, mode: Mode,
text: &'static str, text: &'static str,
current_mode: Mode, current_mode: Mode,
theme: &theme::ChannelModal, theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>, cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> { ) -> AnyElement<ChannelModal> {
let active = mode == current_mode; let active = mode == current_mode;
MouseEventHandler::<T, _>::new(0, cx, move |state, _| { MouseEventHandler::<T, _>::new(0, cx, move |state, _| {
let contained_text = theme.mode_button.style_for(active, state); let contained_text = theme.tab_button.style_for(active, state);
Label::new(text, contained_text.text.clone()) Label::new(text, contained_text.text.clone())
.contained() .contained()
.with_style(contained_text.container.clone()) .with_style(contained_text.container.clone())
@ -367,11 +367,17 @@ impl PickerDelegate for ChannelModalDelegate {
selected: bool, selected: bool,
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = &theme::current(cx).collab_panel.channel_modal; 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, admin) = self.user_at_index(ix).unwrap(); let (user, admin) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx); let request_status = self.member_status(user.id, cx);
let style = theme.picker.item.in_state(selected).style_for(mouse_state); let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
let in_manage = matches!(self.mode, Mode::ManageMembers); let in_manage = matches!(self.mode, Mode::ManageMembers);
@ -448,7 +454,7 @@ impl PickerDelegate for ChannelModalDelegate {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained() .constrained()
.with_height(theme.row_height) .with_height(tabbed_modal.row_height)
.into_any(); .into_any();
if selected { if selected {

View file

@ -1,28 +1,127 @@
use client::{ContactRequestStatus, User, UserStore}; use client::{ContactRequestStatus, User, UserStore};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; use gpui::{
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc; use std::sync::Arc;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx); Picker::<ContactFinderDelegate>::init(cx);
} }
pub type ContactFinder = Picker<ContactFinderDelegate>; pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
pub fn build_contact_finder( impl ContactFinder {
user_store: ModelHandle<UserStore>, pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
cx: &mut ViewContext<ContactFinder>, let picker = cx.add_view(|cx| {
) -> ContactFinder { Picker::new(
Picker::new( ContactFinderDelegate {
ContactFinderDelegate { user_store,
user_store, potential_contacts: Arc::from([]),
potential_contacts: Arc::from([]), selected_index: 0,
selected_index: 0, },
}, cx,
cx, )
) .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
.with_theme(|theme| theme.picker.clone()) });
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
}
impl Entity for ContactFinder {
type Event = PickerEvent;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.tabbed_modal;
fn render_mode_button(
text: &'static str,
theme: &theme::TabbedModal,
_cx: &mut ViewContext<ContactFinder>,
) -> AnyElement<ContactFinder> {
let contained_text = &theme.tab_button.active_state().default;
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new("Contacts", theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([render_mode_button(
"Invite new contacts",
&theme,
cx,
)]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ContactFinder {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
} }
pub struct ContactFinderDelegate { pub struct ContactFinderDelegate {
@ -97,7 +196,9 @@ impl PickerDelegate for ContactFinderDelegate {
selected: bool, selected: bool,
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = &theme::current(cx).contact_finder; let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.contact_finder;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let user = &self.potential_contacts[ix]; let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user); let request_status = self.user_store.read(cx).contact_request_status(user);
@ -113,7 +214,11 @@ impl PickerDelegate for ContactFinderDelegate {
} else { } else {
&theme.contact_button &theme.contact_button
}; };
let style = theme.picker.item.in_state(selected).style_for(mouse_state); let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
Flex::row() Flex::row()
.with_children(user.avatar.clone().map(|avatar| { .with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar) Image::from_data(avatar)
@ -145,7 +250,7 @@ impl PickerDelegate for ContactFinderDelegate {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained() .constrained()
.with_height(theme.row_height) .with_height(tabbed_modal.row_height)
.into_any() .into_any()
} }
} }

View file

@ -48,7 +48,6 @@ pub struct Theme {
pub collab_panel: CollabPanel, pub collab_panel: CollabPanel,
pub project_panel: ProjectPanel, pub project_panel: ProjectPanel,
pub command_palette: CommandPalette, pub command_palette: CommandPalette,
pub contact_finder: ContactFinder,
pub picker: Picker, pub picker: Picker,
pub editor: Editor, pub editor: Editor,
pub search: Search, pub search: Search,
@ -224,6 +223,8 @@ pub struct CollabPanel {
pub log_in_button: Interactive<ContainedText>, pub log_in_button: Interactive<ContainedText>,
pub channel_editor: ContainerStyle, pub channel_editor: ContainerStyle,
pub channel_hash: Icon, pub channel_hash: Icon,
pub tabbed_modal: TabbedModal,
pub contact_finder: ContactFinder,
pub channel_modal: ChannelModal, pub channel_modal: ChannelModal,
pub user_query_editor: FieldEditor, pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32, pub user_query_editor_height: f32,
@ -251,13 +252,20 @@ pub struct CollabPanel {
} }
#[derive(Deserialize, Default, JsonSchema)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelModal { pub struct TabbedModal {
pub tab_button: Toggleable<Interactive<ContainedText>>,
pub modal: ContainerStyle,
pub header: ContainerStyle,
pub body: ContainerStyle,
pub title: ContainedText,
pub picker: Picker,
pub max_height: f32, pub max_height: f32,
pub max_width: f32, pub max_width: f32,
pub title: ContainedText,
pub mode_button: Toggleable<Interactive<ContainedText>>,
pub picker: Picker,
pub row_height: f32, pub row_height: f32,
}
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelModal {
pub contact_avatar: ImageStyle, pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle, pub contact_username: ContainerStyle,
pub remove_member_button: ContainedText, pub remove_member_button: ContainedText,
@ -265,9 +273,6 @@ pub struct ChannelModal {
pub member_icon: Icon, pub member_icon: Icon,
pub invitee_icon: Icon, pub invitee_icon: Icon,
pub member_tag: ContainedText, pub member_tag: ContainedText,
pub modal: ContainerStyle,
pub header: ContainerStyle,
pub body: ContainerStyle,
} }
#[derive(Deserialize, Default, JsonSchema)] #[derive(Deserialize, Default, JsonSchema)]
@ -286,8 +291,6 @@ pub struct TreeBranch {
#[derive(Deserialize, Default, JsonSchema)] #[derive(Deserialize, Default, JsonSchema)]
pub struct ContactFinder { pub struct ContactFinder {
pub picker: Picker,
pub row_height: f32,
pub contact_avatar: ImageStyle, pub contact_avatar: ImageStyle,
pub contact_username: ContainerStyle, pub contact_username: ContainerStyle,
pub contact_button: IconButton, pub contact_button: IconButton,

View file

@ -256,7 +256,7 @@ impl PickerDelegate for BranchListDelegate {
.contained() .contained()
.with_style(style.container) .with_style(style.container)
.constrained() .constrained()
.with_height(theme.contact_finder.row_height) .with_height(theme.collab_panel.tabbed_modal.row_height)
.into_any() .into_any()
} }
fn render_header( fn render_header(

View file

@ -46,7 +46,6 @@ export default function app(): any {
project_diagnostics: project_diagnostics(), project_diagnostics: project_diagnostics(),
project_panel: project_panel(), project_panel: project_panel(),
collab_panel: collab_panel(), collab_panel: collab_panel(),
contact_finder: contact_finder(),
toolbar_dropdown_menu: toolbar_dropdown_menu(), toolbar_dropdown_menu: toolbar_dropdown_menu(),
search: search(), search: search(),
shared_screen: shared_screen(), shared_screen: shared_screen(),

View file

@ -1,153 +0,0 @@
import { useTheme } from "../theme"
import { background, border, foreground, text } from "./components"
import picker from "./picker"
import { input } from "../component/input"
import { toggleable_text_button } from "../component/text_button"
export default function channel_modal(): any {
const theme = useTheme()
const side_margin = 6
const contact_button = {
background: background(theme.middle, "variant"),
color: foreground(theme.middle, "variant"),
icon_width: 8,
button_width: 16,
corner_radius: 8,
}
const picker_style = picker()
delete picker_style.shadow
delete picker_style.border
const picker_input = input()
return {
header: {
background: background(theme.middle, "accent"),
border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }),
corner_radii: {
top_right: 12,
top_left: 12,
}
},
body: {
background: background(theme.middle),
corner_radii: {
bottom_right: 12,
bottom_left: 12,
}
},
modal: {
background: background(theme.middle),
shadow: theme.modal_shadow,
corner_radius: 12,
padding: {
bottom: 0,
left: 0,
right: 0,
top: 0,
},
},
// This is used for the icons that are rendered to the right of channel Members in both UIs
member_icon: {
background: background(theme.middle),
padding: {
bottom: 4,
left: 4,
right: 4,
top: 4,
},
width: 5,
color: foreground(theme.middle, "accent"),
},
// This is used for the icons that are rendered to the right of channel invites in both UIs
invitee_icon: {
background: background(theme.middle),
padding: {
bottom: 4,
left: 4,
right: 4,
top: 4,
},
width: 5,
color: foreground(theme.middle, "accent"),
},
remove_member_button: {
...text(theme.middle, "sans", { size: "xs" }),
background: background(theme.middle),
padding: {
left: 7,
right: 7
}
},
cancel_invite_button: {
...text(theme.middle, "sans", { size: "xs" }),
background: background(theme.middle),
},
member_tag: {
...text(theme.middle, "sans", { size: "xs" }),
border: border(theme.middle, "active"),
background: background(theme.middle),
margin: {
left: 8,
},
padding: {
left: 4,
right: 4,
}
},
max_height: 400,
max_width: 540,
title: {
...text(theme.middle, "sans", "on", { size: "lg" }),
padding: {
left: 6,
}
},
mode_button: toggleable_text_button(theme, {
variant: "ghost",
layer: theme.middle,
active_color: "accent",
margin: {
top: 8,
bottom: 8,
right: 4
}
}),
picker: {
empty_container: {},
item: {
...picker_style.item,
margin: { left: side_margin, right: side_margin },
},
no_matches: picker_style.no_matches,
input_editor: picker_input,
empty_input_editor: picker_input,
header: picker_style.header,
footer: picker_style.footer,
},
row_height: 28,
contact_avatar: {
corner_radius: 10,
width: 18,
},
contact_username: {
padding: {
left: 8,
},
},
contact_button: {
...contact_button,
hover: {
background: background(theme.middle, "variant", "hovered"),
},
},
disabled_contact_button: {
...contact_button,
background: background(theme.middle, "disabled"),
color: foreground(theme.middle, "disabled"),
},
}
}

View file

@ -0,0 +1,159 @@
import { useTheme } from "../theme"
import { background, border, foreground, text } from "./components"
import picker from "./picker"
import { input } from "../component/input"
import { toggleable_text_button } from "../component/text_button"
import contact_finder from "./contact_finder"
export default function channel_modal(): any {
const theme = useTheme()
const side_margin = 6
const contact_button = {
background: background(theme.middle, "variant"),
color: foreground(theme.middle, "variant"),
icon_width: 8,
button_width: 16,
corner_radius: 8,
}
const picker_style = picker()
delete picker_style.shadow
delete picker_style.border
const picker_input = input()
return {
contact_finder: contact_finder(),
tabbed_modal: {
tab_button: toggleable_text_button(theme, {
variant: "ghost",
layer: theme.middle,
active_color: "accent",
margin: {
top: 8,
bottom: 8,
right: 4
}
}),
row_height: 28,
header: {
background: background(theme.middle, "accent"),
border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }),
corner_radii: {
top_right: 12,
top_left: 12,
}
},
body: {
background: background(theme.middle),
corner_radii: {
bottom_right: 12,
bottom_left: 12,
}
},
modal: {
background: background(theme.middle),
shadow: theme.modal_shadow,
corner_radius: 12,
padding: {
bottom: 0,
left: 0,
right: 0,
top: 0,
},
},
max_height: 400,
max_width: 540,
title: {
...text(theme.middle, "sans", "on", { size: "lg" }),
padding: {
left: 6,
}
},
picker: {
empty_container: {},
item: {
...picker_style.item,
margin: { left: side_margin, right: side_margin },
},
no_matches: picker_style.no_matches,
input_editor: picker_input,
empty_input_editor: picker_input,
header: picker_style.header,
footer: picker_style.footer,
},
},
channel_modal: {
// This is used for the icons that are rendered to the right of channel Members in both UIs
member_icon: {
background: background(theme.middle),
padding: {
bottom: 4,
left: 4,
right: 4,
top: 4,
},
width: 5,
color: foreground(theme.middle, "accent"),
},
// This is used for the icons that are rendered to the right of channel invites in both UIs
invitee_icon: {
background: background(theme.middle),
padding: {
bottom: 4,
left: 4,
right: 4,
top: 4,
},
width: 5,
color: foreground(theme.middle, "accent"),
},
remove_member_button: {
...text(theme.middle, "sans", { size: "xs" }),
background: background(theme.middle),
padding: {
left: 7,
right: 7
}
},
cancel_invite_button: {
...text(theme.middle, "sans", { size: "xs" }),
background: background(theme.middle),
},
member_tag: {
...text(theme.middle, "sans", { size: "xs" }),
border: border(theme.middle, "active"),
background: background(theme.middle),
margin: {
left: 8,
},
padding: {
left: 4,
right: 4,
}
},
contact_avatar: {
corner_radius: 10,
width: 18,
},
contact_username: {
padding: {
left: 8,
},
},
contact_button: {
...contact_button,
hover: {
background: background(theme.middle, "variant", "hovered"),
},
},
disabled_contact_button: {
...contact_button,
background: background(theme.middle, "disabled"),
color: foreground(theme.middle, "disabled"),
},
}
}
}

View file

@ -7,9 +7,7 @@ import {
} from "./components" } from "./components"
import { interactive, toggleable } from "../element" import { interactive, toggleable } from "../element"
import { useTheme } from "../theme" import { useTheme } from "../theme"
import channel_modal from "./channel_modal" import collab_modals from "./collab_modals"
import { icon_button, toggleable_icon_button } from "../component/icon_button"
export default function contacts_panel(): any { export default function contacts_panel(): any {
const theme = useTheme() const theme = useTheme()
@ -109,7 +107,7 @@ export default function contacts_panel(): any {
return { return {
channel_modal: channel_modal(), ...collab_modals(),
log_in_button: interactive({ log_in_button: interactive({
base: { base: {
background: background(theme.middle), background: background(theme.middle),

View file

@ -1,11 +1,11 @@
import picker from "./picker" // import picker from "./picker"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
import { useTheme } from "../theme" import { useTheme } from "../theme"
export default function contact_finder(): any { export default function contact_finder(): any {
const theme = useTheme() const theme = useTheme()
const side_margin = 6 // const side_margin = 6
const contact_button = { const contact_button = {
background: background(theme.middle, "variant"), background: background(theme.middle, "variant"),
color: foreground(theme.middle, "variant"), color: foreground(theme.middle, "variant"),
@ -14,42 +14,42 @@ export default function contact_finder(): any {
corner_radius: 8, corner_radius: 8,
} }
const picker_style = picker() // const picker_style = picker()
const picker_input = { // const picker_input = {
background: background(theme.middle, "on"), // background: background(theme.middle, "on"),
corner_radius: 6, // corner_radius: 6,
text: text(theme.middle, "mono"), // text: text(theme.middle, "mono"),
placeholder_text: text(theme.middle, "mono", "on", "disabled", { // placeholder_text: text(theme.middle, "mono", "on", "disabled", {
size: "xs", // size: "xs",
}), // }),
selection: theme.players[0], // selection: theme.players[0],
border: border(theme.middle), // border: border(theme.middle),
padding: { // padding: {
bottom: 4, // bottom: 4,
left: 8, // left: 8,
right: 8, // right: 8,
top: 4, // top: 4,
}, // },
margin: { // margin: {
left: side_margin, // left: side_margin,
right: side_margin, // right: side_margin,
}, // },
} // }
return { return {
picker: { // picker: {
empty_container: {}, // empty_container: {},
item: { // item: {
...picker_style.item, // ...picker_style.item,
margin: { left: side_margin, right: side_margin }, // margin: { left: side_margin, right: side_margin },
}, // },
no_matches: picker_style.no_matches, // no_matches: picker_style.no_matches,
input_editor: picker_input, // input_editor: picker_input,
empty_input_editor: picker_input, // empty_input_editor: picker_input,
header: picker_style.header, // header: picker_style.header,
footer: picker_style.footer, // footer: picker_style.footer,
}, // },
row_height: 28, // row_height: 28,
contact_avatar: { contact_avatar: {
corner_radius: 10, corner_radius: 10,
width: 18, width: 18,