Lay-out channel modal with picker beneath channel name and mode buttons
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
a7e883d956
commit
4a6c73c6fd
4 changed files with 213 additions and 48 deletions
|
@ -1,48 +1,175 @@
|
||||||
use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore};
|
use client::{proto, ChannelId, ChannelStore, User, UserId, UserStore};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
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 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::<ChannelModalDelegate>::init(cx);
|
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(
|
pub fn build_channel_modal(
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
channel: ChannelId,
|
channel_id: ChannelId,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
members: Vec<(Arc<User>, proto::channel_member::Kind)>,
|
members: Vec<(Arc<User>, proto::channel_member::Kind)>,
|
||||||
cx: &mut ViewContext<ChannelModal>,
|
cx: &mut ViewContext<ChannelModal>,
|
||||||
) -> ChannelModal {
|
) -> ChannelModal {
|
||||||
Picker::new(
|
let picker = cx.add_view(|cx| {
|
||||||
ChannelModalDelegate {
|
Picker::new(
|
||||||
matches: Vec::new(),
|
ChannelModalDelegate {
|
||||||
selected_index: 0,
|
matches: Vec::new(),
|
||||||
user_store,
|
selected_index: 0,
|
||||||
channel_store,
|
user_store: user_store.clone(),
|
||||||
channel_id: channel,
|
channel_store: channel_store.clone(),
|
||||||
match_candidates: members
|
channel_id,
|
||||||
.iter()
|
match_candidates: members
|
||||||
.enumerate()
|
.iter()
|
||||||
.map(|(id, member)| StringMatchCandidate {
|
.enumerate()
|
||||||
id,
|
.map(|(id, member)| StringMatchCandidate {
|
||||||
string: member.0.github_login.clone(),
|
id,
|
||||||
char_bag: member.0.github_login.chars().collect(),
|
string: member.0.github_login.clone(),
|
||||||
})
|
char_bag: member.0.github_login.chars().collect(),
|
||||||
.collect(),
|
})
|
||||||
members,
|
.collect(),
|
||||||
mode,
|
members,
|
||||||
},
|
mode,
|
||||||
cx,
|
},
|
||||||
)
|
cx,
|
||||||
.with_theme(|theme| theme.picker.clone())
|
)
|
||||||
|
.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 {
|
pub enum Mode {
|
||||||
ManageMembers,
|
ManageMembers,
|
||||||
InviteMembers,
|
InviteMembers,
|
||||||
|
@ -159,28 +286,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
cx.emit(PickerEvent::Dismiss);
|
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(
|
fn render_match(
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
|
|
|
@ -13,6 +13,7 @@ use std::{cmp, sync::Arc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Modal;
|
use workspace::Modal;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub enum PickerEvent {
|
pub enum PickerEvent {
|
||||||
Dismiss,
|
Dismiss,
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,10 @@ pub struct CollabPanel {
|
||||||
|
|
||||||
#[derive(Deserialize, Default, JsonSchema)]
|
#[derive(Deserialize, Default, JsonSchema)]
|
||||||
pub struct ChannelModal {
|
pub struct ChannelModal {
|
||||||
|
pub container: ContainerStyle,
|
||||||
|
pub height: f32,
|
||||||
|
pub header: TextStyle,
|
||||||
|
pub mode_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub picker: Picker,
|
pub picker: Picker,
|
||||||
pub row_height: f32,
|
pub row_height: f32,
|
||||||
pub contact_avatar: ImageStyle,
|
pub contact_avatar: ImageStyle,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useTheme } from "../theme"
|
import { useTheme } from "../theme"
|
||||||
|
import { interactive, toggleable } from "../element"
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
import picker from "./picker"
|
import picker from "./picker"
|
||||||
|
|
||||||
export default function contacts_panel(): any {
|
export default function channel_modal(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const side_margin = 6
|
const side_margin = 6
|
||||||
|
@ -15,6 +16,9 @@ export default function contacts_panel(): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
const picker_style = picker()
|
const picker_style = picker()
|
||||||
|
delete picker_style.shadow
|
||||||
|
delete picker_style.border
|
||||||
|
|
||||||
const picker_input = {
|
const picker_input = {
|
||||||
background: background(theme.middle, "on"),
|
background: background(theme.middle, "on"),
|
||||||
corner_radius: 6,
|
corner_radius: 6,
|
||||||
|
@ -37,6 +41,57 @@ export default function contacts_panel(): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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: {
|
picker: {
|
||||||
empty_container: {},
|
empty_container: {},
|
||||||
item: {
|
item: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue