Add `StatusBar` component

Marshall Bowers created

Change summary

crates/ui2/src/components.rs            |   2 
crates/ui2/src/components/status_bar.rs | 145 +++++++++++++++++++++++++++
crates/ui2/src/components/workspace.rs  |   4 
crates/ui2/src/elements.rs              |   2 
crates/ui2/src/elements/tool_divider.rs |  23 ++++
5 files changed, 174 insertions(+), 2 deletions(-)

Detailed changes

crates/ui2/src/components.rs 🔗

@@ -4,6 +4,7 @@ mod icon_button;
 mod list;
 mod panel;
 mod project_panel;
+mod status_bar;
 mod workspace;
 
 pub use assistant_panel::*;
@@ -12,4 +13,5 @@ pub use icon_button::*;
 pub use list::*;
 pub use panel::*;
 pub use project_panel::*;
+pub use status_bar::*;
 pub use workspace::*;

crates/ui2/src/components/status_bar.rs 🔗

@@ -0,0 +1,145 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::theme::{theme, Theme};
+use crate::{Icon, IconButton, IconColor, ToolDivider};
+
+#[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<S: 'static + Send + Sync> {
+    state_type: PhantomData<S>,
+    left_tools: Option<ToolGroup>,
+    right_tools: Option<ToolGroup>,
+    bottom_tools: Option<ToolGroup>,
+}
+
+impl<S: 'static + Send + Sync> StatusBar<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+            left_tools: None,
+            right_tools: None,
+            bottom_tools: None,
+        }
+    }
+
+    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, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        div()
+            .py_0p5()
+            .px_1()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_tools(&theme))
+            .child(self.right_tools(&theme))
+    }
+
+    fn left_tools(&self, theme: &Theme) -> impl Element<State = S> {
+        div()
+            .flex()
+            .items_center()
+            .gap_1()
+            .child(IconButton::new(Icon::FileTree).color(IconColor::Accent))
+            .child(IconButton::new(Icon::Hash))
+            .child(ToolDivider::new())
+            .child(IconButton::new(Icon::XCircle))
+    }
+
+    fn right_tools(&self, theme: &Theme) -> impl Element<State = S> {
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    // .child(Button::new("116:25"))
+                    // .child(Button::new("Rust")),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(IconButton::new(Icon::Copilot))
+                    .child(IconButton::new(Icon::Envelope)),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(IconButton::new(Icon::Terminal))
+                    .child(IconButton::new(Icon::MessageBubbles))
+                    .child(IconButton::new(Icon::Ai)),
+            )
+    }
+}

crates/ui2/src/components/workspace.rs 🔗

@@ -5,7 +5,7 @@ use chrono::DateTime;
 use gpui3::{relative, rems, Size};
 
 use crate::prelude::*;
-use crate::{theme, v_stack, Panel, PanelAllowedSides, PanelSide, ProjectPanel};
+use crate::{theme, v_stack, Panel, PanelAllowedSides, PanelSide, ProjectPanel, StatusBar};
 
 #[derive(Element)]
 pub struct WorkspaceElement<S: 'static + Send + Sync + Clone> {
@@ -185,7 +185,7 @@ impl<S: 'static + Send + Sync + Clone> WorkspaceElement<S> {
                        //     .side(PanelSide::Right),
                        // ),
             )
-            // .child(StatusBar::new())
+            .child(StatusBar::new())
         // An example of a toast is below
         // Currently because of stacking order this gets obscured by other elements
 

crates/ui2/src/elements.rs 🔗

@@ -4,6 +4,7 @@ mod icon;
 mod input;
 mod label;
 mod stack;
+mod tool_divider;
 
 pub use avatar::*;
 pub use button::*;
@@ -11,3 +12,4 @@ pub use icon::*;
 pub use input::*;
 pub use label::*;
 pub use stack::*;
+pub use tool_divider::*;

crates/ui2/src/elements/tool_divider.rs 🔗

@@ -0,0 +1,23 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element)]
+pub struct ToolDivider<S: 'static + Send + Sync> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> ToolDivider<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        div().w_px().h_3().fill(theme.lowest.base.default.border)
+    }
+}