diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 52711281c7..5623ada42d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -55,7 +55,7 @@ struct RemoveChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ToggleCollapsed { +struct ToggleCollapse { channel_id: u64, } @@ -79,7 +79,16 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -89,7 +98,7 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapsed + ToggleCollapse ] ); @@ -113,6 +122,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel) } #[derive(Debug)] @@ -1356,7 +1367,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1633,7 +1644,7 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() @@ -1863,6 +1874,12 @@ impl CollabPanel { OverlayPositionMode::Window }); + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu.show( position.unwrap_or_default(), if self.context_menu_on_selected { @@ -1871,6 +1888,7 @@ impl CollabPanel { gpui::elements::AnchorCorner::BottomLeft }, vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), @@ -1950,7 +1968,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2038,7 +2056,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2047,8 +2065,37 @@ impl CollabPanel { self.update_entries(false, cx); } - fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -2057,8 +2104,9 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; - self.update_entries(false, cx); + self.update_entries(true, cx); cx.notify(); + cx.focus_self(); } fn is_channel_collapsed(&self, channel: ChannelId) -> bool { @@ -2104,6 +2152,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fc208954cf..8eaab91fe6 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -4,7 +4,8 @@ use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; -pub type ToggleIconButtonStyle = Toggleable>>; +pub type IconButtonStyle = Interactive>; +pub type ToggleIconButtonStyle = Toggleable; pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; @@ -27,17 +28,19 @@ impl ComponentExt for C { pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, SafeStylable}, + elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { - pub button: ToggleIconButtonStyle, + pub button: IconButtonStyle, + #[serde(flatten)] + pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, @@ -99,6 +102,7 @@ pub mod disclosure { impl Component for Disclosable> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() + .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { Button::dynamic_action(self.action) .with_id(self.id) @@ -107,7 +111,6 @@ pub mod disclosure { } else { "icons/file_icons/chevron_right.svg" })) - .toggleable(disclosed) .with_style(self.style.button) .element() .into_any() @@ -119,7 +122,6 @@ pub mod disclosure { .with_width(self.style.button.button_width.unwrap()) .into_any() }) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) .with_child( self.content .with_style(self.style.content) @@ -127,6 +129,8 @@ pub mod disclosure { .flex(1., true), ) .align_children_center() + .contained() + .with_style(self.style.container) .into_any() } } diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 1a2d0bcec4..935909afdb 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO } const padding = { - top: size === Button.size.Small ? 0 : 2, - bottom: size === Button.size.Small ? 0 : 2, - left: size === Button.size.Small ? 0 : 4, - right: size === Button.size.Small ? 0 : 4, + top: size === Button.size.Small ? 2 : 2, + bottom: size === Button.size.Small ? 2 : 2, + left: size === Button.size.Small ? 2 : 4, + right: size === Button.size.Small ? 2 : 4, } return interactive({ @@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 14, + icon_width: 12, icon_height: 14, - button_width: 20, - button_height: 16, + button_width: size === Button.size.Small ? 16 : 20, + button_height: 14, }, state: { default: { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 5242f90c8d..07f367c8af 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -14,6 +14,7 @@ import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() + const CHANNEL_SPACING = 4 as const const NAME_MARGIN = 6 as const const SPACING = 12 as const const INDENT_SIZE = 8 as const @@ -153,8 +154,8 @@ export default function contacts_panel(): any { return { ...collab_modals(), disclosure: { - button: toggleable_icon_button(theme, {}), - spacing: 4, + button: icon_button({ variant: "ghost", size: "sm" }), + spacing: CHANNEL_SPACING, }, log_in_button: interactive({ base: { @@ -198,7 +199,7 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE * 2, + channel_indent: INDENT_SIZE * 2 + 2, section_icon_size: 14, header_row: { ...text(layer, "sans", { size: "sm", weight: "bold" }), @@ -268,7 +269,7 @@ export default function contacts_panel(): any { channel_name: { ...text(layer, "sans", { size: "sm" }), margin: { - left: NAME_MARGIN, + left: CHANNEL_SPACING, }, }, list_empty_label_container: { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index eadbb5c2f1..e2bb0915c1 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,18 +1,26 @@ -import { toggle_label_button_style } from "../component/label_button" + import { useTheme } from "../common" import { text_button } from "../component/text_button" -import { toggleable_icon_button } from "../component/icon_button" +import { icon_button } from "../component/icon_button" import { text } from "./components" +import { toggleable } from "../element" export default function contacts_panel(): any { const theme = useTheme() return { button: text_button({}), - toggle: toggle_label_button_style({ active_color: "accent" }), + toggle: toggleable({ + base: text_button({}), + state: { + active: { + ...text_button({ color: "accent" }) + } + } + }), disclosure: { ...text(theme.lowest, "sans", "base"), - button: toggleable_icon_button(theme, {}), + button: icon_button({ variant: "ghost" }), spacing: 4, } }