Lay-out channel modal with picker beneath channel name and mode buttons

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2023-08-03 16:15:29 -07:00
parent a7e883d956
commit 4a6c73c6fd
4 changed files with 213 additions and 48 deletions

View File

@ -1,48 +1,175 @@
use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
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::<ChannelModalDelegate>::init(cx);
}
pub type ChannelModal = Picker<ChannelModalDelegate>;
pub struct ChannelModal {
picker: ViewHandle<Picker<ChannelModalDelegate>>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl Entity for ChannelModal {
type Event = PickerEvent;
}
impl View for ChannelModal {
fn ui_name() -> &'static str {
"ChannelModal"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.channel_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()
};
enum InviteMembers {}
enum ManageMembers {}
fn render_mode_button<T: 'static>(
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::ChannelModal,
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);
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
this.picker.update(cx, |picker, cx| {
picker.delegate_mut().mode = mode;
picker.update_matches(picker.query(cx), cx);
cx.notify();
})
}
})
.with_cursor_style(if active {
CursorStyle::Arrow
} else {
CursorStyle::PointingHand
})
.into_any()
}
Flex::column()
.with_child(Label::new(
format!("#{}", channel.name),
theme.header.clone(),
))
.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,
),
]))
.with_child(ChildView::new(&self.picker, cx))
.constrained()
.with_height(theme.height)
.contained()
.with_style(theme.container)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = true;
}
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,
}
}
}
pub fn build_channel_modal(
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel: ChannelId,
channel_id: ChannelId,
mode: Mode,
members: Vec<(Arc<User>, proto::channel_member::Kind)>,
cx: &mut ViewContext<ChannelModal>,
) -> ChannelModal {
Picker::new(
ChannelModalDelegate {
matches: Vec::new(),
selected_index: 0,
user_store,
channel_store,
channel_id: channel,
match_candidates: members
.iter()
.enumerate()
.map(|(id, member)| StringMatchCandidate {
id,
string: member.0.github_login.clone(),
char_bag: member.0.github_login.chars().collect(),
})
.collect(),
members,
mode,
},
cx,
)
.with_theme(|theme| theme.picker.clone())
let picker = cx.add_view(|cx| {
Picker::new(
ChannelModalDelegate {
matches: Vec::new(),
selected_index: 0,
user_store: user_store.clone(),
channel_store: channel_store.clone(),
channel_id,
match_candidates: members
.iter()
.enumerate()
.map(|(id, member)| StringMatchCandidate {
id,
string: member.0.github_login.clone(),
char_bag: member.0.github_login.chars().collect(),
})
.collect(),
members,
mode,
},
cx,
)
.with_theme(|theme| theme.collab_panel.channel_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
let has_focus = picker.read(cx).has_focus();
ChannelModal {
picker,
channel_store,
channel_id,
has_focus,
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum Mode {
ManageMembers,
InviteMembers,
@ -159,28 +286,6 @@ impl PickerDelegate for ChannelModalDelegate {
cx.emit(PickerEvent::Dismiss);
}
fn render_header(
&self,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<AnyElement<Picker<Self>>> {
let theme = &theme::current(cx).collab_panel.channel_modal;
let operation = match self.mode {
Mode::ManageMembers => "Manage",
Mode::InviteMembers => "Add",
};
self.channel_store
.read(cx)
.channel_for_id(self.channel_id)
.map(|channel| {
Label::new(
format!("{} members for #{}", operation, channel.name),
theme.picker.item.default_style().label.clone(),
)
.into_any()
})
}
fn render_match(
&self,
ix: usize,

View File

@ -13,6 +13,7 @@ use std::{cmp, sync::Arc};
use util::ResultExt;
use workspace::Modal;
#[derive(Clone, Copy)]
pub enum PickerEvent {
Dismiss,
}

View File

@ -247,6 +247,10 @@ pub struct CollabPanel {
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelModal {
pub container: ContainerStyle,
pub height: f32,
pub header: TextStyle,
pub mode_button: Toggleable<Interactive<ContainedText>>,
pub picker: Picker,
pub row_height: f32,
pub contact_avatar: ImageStyle,

View File

@ -1,8 +1,9 @@
import { useTheme } from "../theme"
import { interactive, toggleable } from "../element"
import { background, border, foreground, text } from "./components"
import picker from "./picker"
export default function contacts_panel(): any {
export default function channel_modal(): any {
const theme = useTheme()
const side_margin = 6
@ -15,6 +16,9 @@ export default function contacts_panel(): any {
}
const picker_style = picker()
delete picker_style.shadow
delete picker_style.border
const picker_input = {
background: background(theme.middle, "on"),
corner_radius: 6,
@ -37,6 +41,57 @@ export default function contacts_panel(): any {
}
return {
container: {
background: background(theme.lowest),
border: border(theme.lowest),
shadow: theme.modal_shadow,
corner_radius: 12,
padding: {
bottom: 4,
left: 20,
right: 20,
top: 20,
},
},
height: 400,
header: text(theme.middle, "sans", "on", { size: "lg" }),
mode_button: toggleable({
base: interactive({
base: {
...text(theme.middle, "sans", { size: "xs" }),
border: border(theme.middle, "active"),
corner_radius: 4,
padding: {
top: 3,
bottom: 3,
left: 7,
right: 7,
},
margin: { left: 6, top: 6, bottom: 6 },
},
state: {
hovered: {
...text(theme.middle, "sans", "default", { size: "xs" }),
background: background(theme.middle, "hovered"),
border: border(theme.middle, "active"),
},
},
}),
state: {
active: {
default: {
color: foreground(theme.middle, "accent"),
},
hovered: {
color: foreground(theme.middle, "accent", "hovered"),
},
clicked: {
color: foreground(theme.middle, "accent", "pressed"),
},
},
}
}),
picker: {
empty_container: {},
item: {