Fix `.active()` interaction state

Nate Butler and Nathan Sobo created

Co-Authored-By: Nathan Sobo <1789+nathansobo@users.noreply.github.com>

Change summary

crates/gpui2/src/elements/pressable.rs         |  13 +
crates/storybook/src/components.rs             |   1 
crates/storybook/src/components/icon_button.rs |   2 
crates/storybook/src/components/icon_button2   | 123 --------------------
crates/storybook/src/modules.rs                |   3 
crates/storybook/src/modules/chat_panel.rs     |   6 
crates/storybook/src/modules/status_bar.rs     | 109 +++++++++++++++++
crates/storybook/src/modules/tab_bar.rs        |  10 
crates/storybook/src/modules/title_bar.rs      |  43 ++++++
crates/storybook/src/storybook.rs              |   7 
crates/storybook/src/workspace.rs              |   3 
11 files changed, 175 insertions(+), 145 deletions(-)

Detailed changes

crates/gpui2/src/elements/pressable.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     ViewContext,
 };
 use anyhow::Result;
-use gpui::{geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
+use gpui::{color::Color, geometry::vector::Vector2F, platform::MouseButtonEvent, LayoutId};
 use refineable::{CascadeSlot, Refineable, RefinementCascade};
 use smallvec::SmallVec;
 use std::{cell::Cell, rc::Rc};
@@ -73,10 +73,15 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
                 if bounds.contains_point(event.position) {
                     pressed.set(true);
                     cx.repaint();
+                } else {
+                    cx.bubble_event();
                 }
-            } else if pressed.get() {
-                pressed.set(false);
-                cx.repaint();
+            } else {
+                if pressed.get() {
+                    pressed.set(false);
+                    cx.repaint();
+                }
+                cx.bubble_event();
             }
         });
 

crates/storybook/src/components.rs 🔗

@@ -12,7 +12,6 @@ mod tool_divider;
 pub use avatar::avatar;
 pub use avatar::Avatar;
 pub use icon_button::icon_button;
-pub use icon_button::IconButton;
 pub use tab::tab;
 pub use tab::Tab;
 pub use tool_divider::tool_divider;

crates/storybook/src/components/icon_button.rs 🔗

@@ -12,7 +12,7 @@ pub struct IconButton {
     state: InteractionState,
 }
 
-pub fn icon_button<V: 'static>(path: &'static str) -> IconButton {
+pub fn icon_button(path: &'static str) -> IconButton {
     IconButton {
         path,
         variant: ButtonVariant::default(),

crates/storybook/src/components/icon_button2 🔗

@@ -1,123 +0,0 @@
-// use crate::prelude::{ButtonVariant, UIState};
-// use crate::theme::theme;
-// use gpui2::elements::svg;
-// use gpui2::style::{StyleHelpers, Styleable};
-// use gpui2::{elements::div, IntoElement};
-// use gpui2::{Element, ParentElement, ViewContext};
-
-// #[derive(Element)]
-// pub(crate) struct IconButton {
-//     path: &'static str,
-//     variant: ButtonVariant,
-//     state: UIState,
-// }
-
-// pub fn icon_button<V: 'static>(path: &'static str) -> IconButton {
-//     IconButton {
-//         path,
-//         variant: ButtonVariant::Filled,
-//         state: UIState::Default,
-//     }
-// }
-
-// impl IconButton {
-//     fn variant(mut self, variant: ButtonVariant) -> Self {
-//         self.variant = variant;
-
-//         // Example of more interesting setter behavior
-//         // FilledButtons must be disabled
-//         if self.variant == ButtonVariant::Filled {
-//             self.state = UIState::Disabled;
-//         }
-
-//         self
-//     }
-
-//     fn state(mut self, state: UIState) -> Self {
-//         // Example of more interesting setter behavior:
-//         // GhostButtons Cannot be disabled
-//         // Debug asserts are compiled out when we make a new release.
-//         // Useful for making sure developers develop correctly without breaking
-//         // everything
-//         debug_assert!(self.variant != ButtonVariant::Ghost && state != UIState::Disabled);
-
-//         self.state = state;
-//         self
-//     }
-
-//     // const state = {
-//     // foo: "foo",
-//     // bar: "bar"
-//     // } as const
-//     //
-//     // type State = typeof state[keyof typeof something]
-//     //
-//     // type Button {
-//     //      style: State
-//     // }
-//     //
-//     // <Button style="foo" /> State['foo']
-
-//     fn render_warning<V: 'static>(&mut self) -> impl IntoElement<V> {
-//         div()
-//     }
-
-//     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-//         let theme = theme(cx);
-
-//         let icon_color;
-
-//         enum Severity {
-//             Low,
-//             Medium,
-//             High,
-//         }
-
-//         // Enum declaration and match statement example
-//         enum Style {
-//             Error,
-//             Warning(Severity),
-//             Foo,
-//             Bar,
-//             Baz,
-//         }
-
-//         let style = Style::Warning(Severity::High);
-
-//         match style {
-//             Error => return self.render_warning(),
-//             Warning(severity) => match severity {
-//                 Low => {}
-//                 Medium => {}
-//                 High => {}
-//             },
-//             Foo => {}
-//             Bar => {}
-//             Baz => {}
-//         }
-
-//         if self.state == UIState::Disabled {
-//             icon_color = theme.highest.base.disabled.foreground;
-//         } else {
-//             icon_color = theme.highest.base.default.foreground;
-//         }
-
-//         let mut div = div();
-
-//         if self.variant == ButtonVariant::Filled {
-//             div = div.fill(theme.highest.on.default.background);
-//         }
-
-//         div.w_7()
-//             .h_6()
-//             .flex()
-//             .items_center()
-//             .justify_center()
-//             .rounded_md()
-//             .hover()
-//             .fill(theme.highest.base.hovered.background)
-//             .active()
-//             .fill(theme.highest.base.pressed.background)
-//             .child(svg().path(self.path).w_4().h_4().fill(icon_color))
-//     }
-// }

crates/storybook/src/modules.rs 🔗

@@ -1,7 +1,10 @@
 mod chat_panel;
+mod status_bar;
 mod tab_bar;
 mod title_bar;
 
 pub(crate) use chat_panel::chat_panel;
+pub use status_bar::status_bar;
+pub use status_bar::StatusBar;
 pub(crate) use tab_bar::tab_bar;
 pub(crate) use title_bar::title_bar;

crates/storybook/src/modules/chat_panel.rs 🔗

@@ -1,6 +1,6 @@
 use std::marker::PhantomData;
 
-use crate::components::{icon_button, IconButton};
+use crate::components::icon_button;
 use crate::theme::theme;
 use gpui2::elements::div::ScrollState;
 use gpui2::style::StyleHelpers;
@@ -57,8 +57,8 @@ impl<V: 'static> ChatPanel<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button::<IconButton>("icons/plus.svg"))
-                            .child(icon_button::<IconButton>("icons/split.svg")),
+                            .child(icon_button("icons/plus.svg"))
+                            .child(icon_button("icons/split.svg")),
                     ),
             )
     }

crates/storybook/src/modules/status_bar.rs 🔗

@@ -0,0 +1,109 @@
+use std::marker::PhantomData;
+
+use crate::components::icon_button;
+use crate::theme::{theme, Theme};
+use gpui2::style::StyleHelpers;
+use gpui2::{elements::div, IntoElement};
+use gpui2::{Element, ParentElement, ViewContext};
+
+#[derive(Default, PartialEq)]
+pub enum Tool {
+    #[default]
+    ProjectPanel,
+    CollaborationPanel,
+    Terminal,
+    Assistant,
+    Feedback,
+    Diagnostics,
+}
+
+struct ToolGroup {
+    active_index: Option<usize>,
+    tools: Vec<Tool>,
+}
+
+impl Default for ToolGroup {
+    fn default() -> Self {
+        ToolGroup {
+            active_index: None,
+            tools: vec![],
+        }
+    }
+}
+
+#[derive(Element)]
+pub struct StatusBar<V: 'static> {
+    view_type: PhantomData<V>,
+    left_tools: Option<ToolGroup>,
+    right_tools: Option<ToolGroup>,
+    bottom_tools: Option<ToolGroup>,
+}
+
+pub fn status_bar<V: 'static>() -> StatusBar<V> {
+    StatusBar {
+        view_type: PhantomData,
+        left_tools: None,
+        right_tools: None,
+        bottom_tools: None,
+    }
+}
+
+impl<V: 'static> StatusBar<V> {
+    pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.left_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.right_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.bottom_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_tools(theme))
+            .child(div())
+    }
+
+    fn left_tools(&self, theme: &Theme) -> impl Element<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_px()
+            .child(icon_button("icons/folder_tree_16.svg"))
+            .child(icon_button("icons/bolt_16.svg"))
+    }
+}

crates/storybook/src/modules/tab_bar.rs 🔗

@@ -1,6 +1,6 @@
 use std::marker::PhantomData;
 
-use crate::components::{icon_button, tab, IconButton};
+use crate::components::{icon_button, tab};
 use crate::prelude::InteractionState;
 use crate::theme::theme;
 use gpui2::elements::div::ScrollState;
@@ -43,11 +43,11 @@ impl<V: 'static> TabBar<V> {
                             .items_center()
                             .gap_px()
                             .child(
-                                icon_button::<IconButton>("icons/arrow_left.svg")
+                                icon_button("icons/arrow_left.svg")
                                     .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
                             )
                             .child(
-                                icon_button::<IconButton>("icons/arrow_right.svg").state(
+                                icon_button("icons/arrow_right.svg").state(
                                     InteractionState::Enabled.if_enabled(can_navigate_forward),
                                 ),
                             ),
@@ -83,8 +83,8 @@ impl<V: 'static> TabBar<V> {
                             .flex()
                             .items_center()
                             .gap_px()
-                            .child(icon_button::<IconButton>("icons/plus.svg"))
-                            .child(icon_button::<IconButton>("icons/split.svg")),
+                            .child(icon_button("icons/plus.svg"))
+                            .child(icon_button("icons/split.svg")),
                     ),
             )
     }

crates/storybook/src/modules/title_bar.rs 🔗

@@ -1,6 +1,6 @@
 use std::marker::PhantomData;
 
-use crate::components::{avatar, icon_button, tool_divider, Avatar, IconButton};
+use crate::components::{avatar, icon_button, tool_divider, Avatar};
 use crate::prelude::Shape;
 use crate::theme::theme;
 use gpui2::style::{StyleHelpers, Styleable};
@@ -29,6 +29,37 @@ impl<V: 'static> TitleBar<V> {
             .w_full()
             .h_8()
             .fill(theme.lowest.base.default.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button("icons/stop_sharing.svg"))
+                            .child(icon_button("icons/exit.svg")),
+                    )
+                    .child(tool_divider())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(icon_button("icons/radix/mic.svg"))
+                            .child(icon_button("icons/radix/speaker-loud.svg"))
+                            .child(icon_button("icons/radix/desktop.svg")),
+                    )
+                    .child(
+                        div().px_2().flex().items_center().child(
+                            avatar::<Avatar>("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .shape(Shape::RoundedRectangle),
+                        ),
+                    ),
+            )
             .child(
                 div()
                     .flex()
@@ -111,8 +142,8 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button::<IconButton>("icons/stop_sharing.svg"))
-                            .child(icon_button::<IconButton>("icons/exit.svg")),
+                            .child(icon_button("icons/stop_sharing.svg"))
+                            .child(icon_button("icons/exit.svg")),
                     )
                     .child(tool_divider())
                     .child(
@@ -121,9 +152,9 @@ impl<V: 'static> TitleBar<V> {
                             .flex()
                             .items_center()
                             .gap_1()
-                            .child(icon_button::<IconButton>("icons/radix/mic.svg"))
-                            .child(icon_button::<IconButton>("icons/radix/speaker-loud.svg"))
-                            .child(icon_button::<IconButton>("icons/radix/desktop.svg")),
+                            .child(icon_button("icons/radix/mic.svg"))
+                            .child(icon_button("icons/radix/speaker-loud.svg"))
+                            .child(icon_button("icons/radix/desktop.svg")),
                     )
                     .child(
                         div().px_2().flex().items_center().child(

crates/storybook/src/storybook.rs 🔗

@@ -2,10 +2,15 @@
 
 use crate::theme::Theme;
 use ::theme as legacy_theme;
+use components::icon_button;
 use element_ext::ElementExt;
-use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
+use gpui2::{
+    elements::div, serde_json, style::StyleHelpers, vec2f, view, Element, ParentElement, RectF,
+    ViewContext, WindowBounds,
+};
 use legacy_theme::ThemeSettings;
 use log::LevelFilter;
+use modules::title_bar;
 use settings::{default_settings, SettingsStore};
 use simplelog::SimpleLogger;
 

crates/storybook/src/workspace.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     collab_panel::collab_panel,
-    modules::{chat_panel, tab_bar, title_bar},
+    modules::{chat_panel, status_bar, tab_bar, title_bar},
     theme::theme,
 };
 use gpui2::{
@@ -59,6 +59,7 @@ impl WorkspaceElement {
                     .child(chat_panel(self.right_scroll_state.clone())),
             )
             .child(statusbar())
+            .child(status_bar())
     }
 }