diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index e6a3ba9288..9af6099f65 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -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::::init(cx); } -pub type ChannelModal = Picker; +pub struct ChannelModal { + picker: ViewHandle>, + channel_store: ModelHandle, + 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) -> AnyElement { + 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( + mode: Mode, + text: &'static str, + current_mode: Mode, + theme: &theme::ChannelModal, + cx: &mut ViewContext, + ) -> AnyElement { + let active = mode == current_mode; + MouseEventHandler::::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::( + Mode::InviteMembers, + "Invite members", + mode, + theme, + cx, + ), + render_mode_button::( + 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.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + 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, channel_store: ModelHandle, - channel: ChannelId, + channel_id: ChannelId, mode: Mode, members: Vec<(Arc, proto::channel_member::Kind)>, cx: &mut ViewContext, ) -> 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>, - ) -> Option>> { - 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, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 6efa33e961..ef8b75d1b3 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -13,6 +13,7 @@ use std::{cmp, sync::Arc}; use util::ResultExt; use workspace::Modal; +#[derive(Clone, Copy)] pub enum PickerEvent { Dismiss, } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c557fbcf52..8d0159d7ad 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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>, pub picker: Picker, pub row_height: f32, pub contact_avatar: ImageStyle, diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 3eff0e4b9a..951591676b 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -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: {