ui: Add new component for thread sidebar panel toggle (#51441)

Danilo Leal created

Change summary

assets/icons/thread.svg                                      |   3 
crates/ui/src/components/ai.rs                               |   2 
crates/ui/src/components/ai/configured_api_card.rs           |  55 +
crates/ui/src/components/ai/copilot_configuration_callout.rs |   1 
crates/ui/src/components/ai/thread_sidebar_toggle.rs         | 177 ++++++
5 files changed, 235 insertions(+), 3 deletions(-)

Detailed changes

assets/icons/thread.svg 🔗

@@ -1,3 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path opacity="0.12" d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" fill="#C6CAD0"/>
+<path d="M5.97658 12.549C7.04814 13.0987 8.2808 13.2476 9.45243 12.9688C10.624 12.6901 11.6576 12.0021 12.3668 11.0287C13.076 10.0554 13.4143 8.8607 13.3206 7.66002C13.2269 6.45934 12.7075 5.33159 11.8559 4.48C11.0043 3.62841 9.87664 3.10898 8.67592 3.01531C7.47524 2.92164 6.28059 3.2599 5.30723 3.96912C4.33388 4.67834 3.64584 5.71188 3.3671 6.88351C3.08836 8.05514 3.23724 9.2878 3.78693 10.3594L2.66404 13.6719L5.97658 12.549Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

crates/ui/src/components/ai.rs 🔗

@@ -1,5 +1,7 @@
 mod configured_api_card;
 mod thread_item;
+mod thread_sidebar_toggle;
 
 pub use configured_api_card::*;
 pub use thread_item::*;
+pub use thread_sidebar_toggle::*;

crates/ui/src/components/ai/configured_api_card.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{Tooltip, prelude::*};
 use gpui::{ClickEvent, IntoElement, ParentElement, SharedString};
 
-#[derive(IntoElement)]
+#[derive(IntoElement, RegisterComponent)]
 pub struct ConfiguredApiCard {
     label: SharedString,
     button_label: Option<SharedString>,
@@ -52,6 +52,59 @@ impl ConfiguredApiCard {
     }
 }
 
+impl Component for ConfiguredApiCard {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let container = || {
+            v_flex()
+                .w_72()
+                .p_2()
+                .gap_2()
+                .border_1()
+                .border_color(cx.theme().colors().border_variant)
+                .bg(cx.theme().colors().panel_background)
+        };
+
+        let examples = vec![
+            single_example(
+                "Default",
+                container()
+                    .child(ConfiguredApiCard::new("API key is configured"))
+                    .into_any_element(),
+            ),
+            single_example(
+                "Custom Button Label",
+                container()
+                    .child(
+                        ConfiguredApiCard::new("OpenAI API key configured")
+                            .button_label("Remove Key"),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "With Tooltip",
+                container()
+                    .child(
+                        ConfiguredApiCard::new("Anthropic API key configured")
+                            .tooltip_label("Click to reset your API key"),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Disabled",
+                container()
+                    .child(ConfiguredApiCard::new("API key is configured").disabled(true))
+                    .into_any_element(),
+            ),
+        ];
+
+        Some(example_group(examples).into_any_element())
+    }
+}
+
 impl RenderOnce for ConfiguredApiCard {
     fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
         let button_label = self.button_label.unwrap_or("Reset Key".into());

crates/ui/src/components/ai/thread_sidebar_toggle.rs 🔗

@@ -0,0 +1,177 @@
+use gpui::{AnyView, ClickEvent};
+use ui_macros::RegisterComponent;
+
+use crate::prelude::*;
+use crate::{IconButton, IconName, Tooltip};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct ThreadSidebarToggle {
+    sidebar_selected: bool,
+    thread_selected: bool,
+    flipped: bool,
+    sidebar_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
+    thread_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
+    on_sidebar_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+    on_thread_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+}
+
+impl ThreadSidebarToggle {
+    pub fn new() -> Self {
+        Self {
+            sidebar_selected: false,
+            thread_selected: false,
+            flipped: false,
+            sidebar_tooltip: None,
+            thread_tooltip: None,
+            on_sidebar_click: None,
+            on_thread_click: None,
+        }
+    }
+
+    pub fn sidebar_selected(mut self, selected: bool) -> Self {
+        self.sidebar_selected = selected;
+        self
+    }
+
+    pub fn thread_selected(mut self, selected: bool) -> Self {
+        self.thread_selected = selected;
+        self
+    }
+
+    pub fn flipped(mut self, flipped: bool) -> Self {
+        self.flipped = flipped;
+        self
+    }
+
+    pub fn sidebar_tooltip(
+        mut self,
+        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        self.sidebar_tooltip = Some(Box::new(tooltip));
+        self
+    }
+
+    pub fn thread_tooltip(
+        mut self,
+        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        self.thread_tooltip = Some(Box::new(tooltip));
+        self
+    }
+
+    pub fn on_sidebar_click(
+        mut self,
+        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        self.on_sidebar_click = Some(Box::new(handler));
+        self
+    }
+
+    pub fn on_thread_click(
+        mut self,
+        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        self.on_thread_click = Some(Box::new(handler));
+        self
+    }
+}
+
+impl RenderOnce for ThreadSidebarToggle {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let sidebar_icon = match (self.sidebar_selected, self.flipped) {
+            (true, false) => IconName::ThreadsSidebarLeftOpen,
+            (false, false) => IconName::ThreadsSidebarLeftClosed,
+            (true, true) => IconName::ThreadsSidebarRightOpen,
+            (false, true) => IconName::ThreadsSidebarRightClosed,
+        };
+
+        h_flex()
+            .min_w_0()
+            .rounded_sm()
+            .gap_px()
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .when(self.flipped, |this| this.flex_row_reverse())
+            .child(
+                IconButton::new("sidebar-toggle", sidebar_icon)
+                    .icon_size(IconSize::Small)
+                    .toggle_state(self.sidebar_selected)
+                    .when_some(self.sidebar_tooltip, |this, tooltip| this.tooltip(tooltip))
+                    .when_some(self.on_sidebar_click, |this, handler| {
+                        this.on_click(handler)
+                    }),
+            )
+            .child(div().h_4().w_px().bg(cx.theme().colors().border))
+            .child(
+                IconButton::new("thread-toggle", IconName::Thread)
+                    .icon_size(IconSize::Small)
+                    .toggle_state(self.thread_selected)
+                    .when_some(self.thread_tooltip, |this, tooltip| this.tooltip(tooltip))
+                    .when_some(self.on_thread_click, |this, handler| this.on_click(handler)),
+            )
+    }
+}
+
+impl Component for ThreadSidebarToggle {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let container = || div().p_2().bg(cx.theme().colors().status_bar_background);
+
+        let examples = vec![
+            single_example(
+                "Both Unselected",
+                container()
+                    .child(ThreadSidebarToggle::new())
+                    .into_any_element(),
+            ),
+            single_example(
+                "Sidebar Selected",
+                container()
+                    .child(ThreadSidebarToggle::new().sidebar_selected(true))
+                    .into_any_element(),
+            ),
+            single_example(
+                "Thread Selected",
+                container()
+                    .child(ThreadSidebarToggle::new().thread_selected(true))
+                    .into_any_element(),
+            ),
+            single_example(
+                "Both Selected",
+                container()
+                    .child(
+                        ThreadSidebarToggle::new()
+                            .sidebar_selected(true)
+                            .thread_selected(true),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Flipped",
+                container()
+                    .child(
+                        ThreadSidebarToggle::new()
+                            .sidebar_selected(true)
+                            .thread_selected(true)
+                            .flipped(true),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "With Tooltips",
+                container()
+                    .child(
+                        ThreadSidebarToggle::new()
+                            .sidebar_tooltip(Tooltip::text("Toggle Sidebar"))
+                            .thread_tooltip(Tooltip::text("Toggle Thread")),
+                    )
+                    .into_any_element(),
+            ),
+        ];
+
+        Some(example_group(examples).into_any_element())
+    }
+}