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::{
proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore,
};
use contact_finder::build_contact_finder;
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
use editor::{Cancel, Editor};
@ -46,6 +46,8 @@ use workspace::{
use crate::face_pile::FacePile;
use channel_modal::ChannelModal;
use self::contact_finder::ContactFinder;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct RemoveChannel {
channel_id: u64,
@ -1945,7 +1947,7 @@ impl CollabPanel {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, 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
})

View file

@ -66,7 +66,7 @@ impl ChannelModal {
},
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();
@ -143,7 +143,7 @@ impl View for ChannelModal {
}
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 Some(channel) = self
@ -160,12 +160,12 @@ impl View for ChannelModal {
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::ChannelModal,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
let active = mode == current_mode;
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())
.contained()
.with_style(contained_text.container.clone())
@ -367,11 +367,17 @@ impl PickerDelegate for ChannelModalDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> 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 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);
@ -448,7 +454,7 @@ impl PickerDelegate for ChannelModalDelegate {
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.row_height)
.with_height(tabbed_modal.row_height)
.into_any();
if selected {

View file

@ -1,19 +1,24 @@
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 std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx);
}
pub type ContactFinder = Picker<ContactFinderDelegate>;
pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
pub fn build_contact_finder(
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<ContactFinder>,
) -> ContactFinder {
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,
@ -22,7 +27,101 @@ pub fn build_contact_finder(
},
cx,
)
.with_theme(|theme| theme.picker.clone())
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
}
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 {
@ -97,7 +196,9 @@ impl PickerDelegate for ContactFinderDelegate {
selected: bool,
cx: &gpui::AppContext,
) -> 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 request_status = self.user_store.read(cx).contact_request_status(user);
@ -113,7 +214,11 @@ impl PickerDelegate for ContactFinderDelegate {
} else {
&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()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
@ -145,7 +250,7 @@ impl PickerDelegate for ContactFinderDelegate {
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.row_height)
.with_height(tabbed_modal.row_height)
.into_any()
}
}

View file

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

View file

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

View file

@ -46,7 +46,6 @@ export default function app(): any {
project_diagnostics: project_diagnostics(),
project_panel: project_panel(),
collab_panel: collab_panel(),
contact_finder: contact_finder(),
toolbar_dropdown_menu: toolbar_dropdown_menu(),
search: search(),
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"
import { interactive, toggleable } from "../element"
import { useTheme } from "../theme"
import channel_modal from "./channel_modal"
import { icon_button, toggleable_icon_button } from "../component/icon_button"
import collab_modals from "./collab_modals"
export default function contacts_panel(): any {
const theme = useTheme()
@ -109,7 +107,7 @@ export default function contacts_panel(): any {
return {
channel_modal: channel_modal(),
...collab_modals(),
log_in_button: interactive({
base: {
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 { useTheme } from "../theme"
export default function contact_finder(): any {
const theme = useTheme()
const side_margin = 6
// const side_margin = 6
const contact_button = {
background: background(theme.middle, "variant"),
color: foreground(theme.middle, "variant"),
@ -14,42 +14,42 @@ export default function contact_finder(): any {
corner_radius: 8,
}
const picker_style = picker()
const picker_input = {
background: background(theme.middle, "on"),
corner_radius: 6,
text: text(theme.middle, "mono"),
placeholder_text: text(theme.middle, "mono", "on", "disabled", {
size: "xs",
}),
selection: theme.players[0],
border: border(theme.middle),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: side_margin,
right: side_margin,
},
}
// const picker_style = picker()
// const picker_input = {
// background: background(theme.middle, "on"),
// corner_radius: 6,
// text: text(theme.middle, "mono"),
// placeholder_text: text(theme.middle, "mono", "on", "disabled", {
// size: "xs",
// }),
// selection: theme.players[0],
// border: border(theme.middle),
// padding: {
// bottom: 4,
// left: 8,
// right: 8,
// top: 4,
// },
// margin: {
// left: side_margin,
// right: side_margin,
// },
// }
return {
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,
// 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,