Button2 – Part1 (#3420)

Nate Butler and Marshall Bowers created

## TODO

- [x] Remove `InteractionState`
- [ ] `Selectable` should use `Selection` instead of a boolean
- [x] Clean out ui2 prelude
- [ ] Build out button2 button types
- [ ] Port old buttons

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>

Change summary

crates/collab_ui2/src/collab_titlebar_item.rs |  26 +
crates/storybook2/src/stories/focus.rs        |   2 
crates/storybook2/src/stories/picker.rs       |   2 
crates/storybook2/src/stories/scroll.rs       |   2 
crates/storybook2/src/story_selector.rs       |   2 
crates/ui2/src/clickable.rs                   |   5 
crates/ui2/src/components.rs                  |   4 
crates/ui2/src/components/button2.rs          | 413 +++++++++++++++++++++
crates/ui2/src/components/icon_button.rs      |  18 
crates/ui2/src/components/input.rs            | 108 -----
crates/ui2/src/components/stories.rs          |   3 
crates/ui2/src/components/stories/button.rs   | 164 ++-----
crates/ui2/src/components/stories/input.rs    |  18 
crates/ui2/src/fixed.rs                       |   6 
crates/ui2/src/prelude.rs                     |  59 --
crates/ui2/src/selectable.rs                  |  26 +
crates/ui2/src/styles/color.rs                |   2 
crates/ui2/src/ui2.rs                         |   6 
crates/workspace2/src/dock.rs                 |  24 
crates/workspace2/src/pane.rs                 |  12 
crates/workspace2/src/status_bar.rs           |   2 
crates/workspace2/src/toolbar.rs              |   2 
22 files changed, 567 insertions(+), 339 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -37,7 +37,10 @@ use gpui::{
 };
 use project::Project;
 use theme::ActiveTheme;
-use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
+use ui::{
+    h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
+    IconElement, IconSize, KeyBinding, Tooltip,
+};
 use util::ResultExt;
 use workspace::{notifications::NotifyResultExt, Workspace};
 
@@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem {
                         })
                         .detach();
                     }))
+                    // Temporary, will be removed when the last part of button2 is merged
+                    .child(
+                        div().border().border_color(gpui::blue()).child(
+                            ButtonLike::new("test-button")
+                                .children([
+                                    Avatar::uri(
+                                        "https://avatars.githubusercontent.com/u/1714999?v=4",
+                                    )
+                                    .into_element()
+                                    .into_any(),
+                                    IconElement::new(ui::Icon::ChevronDown)
+                                        .size(IconSize::Small)
+                                        .into_element()
+                                        .into_any(),
+                                ])
+                                .on_click(move |event, _cx| {
+                                    dbg!(format!("clicked: {:?}", event.down.position));
+                                })
+                                .tooltip(|cx| Tooltip::text("Test tooltip", cx)),
+                        ),
+                    )
                 }
             })
     }

crates/storybook2/src/stories/focus.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{
     actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
     WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 
 actions!(ActionA, ActionB, ActionC);
 

crates/storybook2/src/stories/picker.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{Label, ListItem};
 
 pub struct PickerStory {

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::Tooltip;
 
 pub struct ScrollStory;

crates/storybook2/src/story_selector.rs 🔗

@@ -19,7 +19,6 @@ pub enum ComponentStory {
     Focus,
     Icon,
     IconButton,
-    Input,
     Keybinding,
     Label,
     ListItem,
@@ -39,7 +38,6 @@ impl ComponentStory {
             Self::Focus => FocusStory::view(cx).into(),
             Self::Icon => cx.build_view(|_| ui::IconStory).into(),
             Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
-            Self::Input => cx.build_view(|_| ui::InputStory).into(),
             Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
             Self::Label => cx.build_view(|_| ui::LabelStory).into(),
             Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),

crates/ui2/src/clickable.rs 🔗

@@ -0,0 +1,5 @@
+use gpui::{ClickEvent, WindowContext};
+
+pub trait Clickable {
+    fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
+}

crates/ui2/src/components.rs 🔗

@@ -1,12 +1,12 @@
 mod avatar;
 mod button;
+mod button2;
 mod checkbox;
 mod context_menu;
 mod disclosure;
 mod divider;
 mod icon;
 mod icon_button;
-mod input;
 mod keybinding;
 mod label;
 mod list;
@@ -21,13 +21,13 @@ mod stories;
 
 pub use avatar::*;
 pub use button::*;
+pub use button2::*;
 pub use checkbox::*;
 pub use context_menu::*;
 pub use disclosure::*;
 pub use divider::*;
 pub use icon::*;
 pub use icon_button::*;
-pub use input::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;

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

@@ -0,0 +1,413 @@
+use gpui::{
+    rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
+    StatefulInteractiveElement, WindowContext,
+};
+use smallvec::SmallVec;
+
+use crate::{h_stack, prelude::*};
+
+// 🚧 Heavily WIP 🚧
+
+// #[derive(Default, PartialEq, Clone, Copy)]
+// pub enum ButtonType2 {
+//     #[default]
+//     DefaultButton,
+//     IconButton,
+//     ButtonLike,
+//     SplitButton,
+//     ToggleButton,
+// }
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum IconPosition2 {
+    #[default]
+    Before,
+    After,
+}
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum ButtonStyle2 {
+    #[default]
+    Filled,
+    // Tinted,
+    Subtle,
+    Transparent,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct ButtonStyle {
+    pub background: Hsla,
+    pub border_color: Hsla,
+    pub label_color: Hsla,
+    pub icon_color: Hsla,
+}
+
+impl ButtonStyle2 {
+    pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_background,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+        }
+    }
+
+    pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_hover,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_hover,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                // TODO: These are not great
+                label_color: Color::Muted.color(cx),
+                // TODO: These are not great
+                icon_color: Color::Muted.color(cx),
+            },
+        }
+    }
+
+    pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_active,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_active,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                // TODO: These are not great
+                label_color: Color::Muted.color(cx),
+                // TODO: These are not great
+                icon_color: Color::Muted.color(cx),
+            },
+        }
+    }
+
+    pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_background,
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Accent.color(cx),
+                icon_color: Color::Accent.color(cx),
+            },
+        }
+    }
+
+    pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum ButtonSize2 {
+    #[default]
+    Default,
+    Compact,
+    None,
+}
+
+impl ButtonSize2 {
+    fn height(self) -> Rems {
+        match self {
+            ButtonSize2::Default => rems(22. / 16.),
+            ButtonSize2::Compact => rems(18. / 16.),
+            ButtonSize2::None => rems(16. / 16.),
+        }
+    }
+}
+
+// pub struct Button {
+//     id: ElementId,
+//     icon: Option<Icon>,
+//     icon_color: Option<Color>,
+//     icon_position: Option<IconPosition2>,
+//     label: Option<Label>,
+//     label_color: Option<Color>,
+//     appearance: ButtonAppearance2,
+//     state: InteractionState,
+//     selected: bool,
+//     disabled: bool,
+//     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+//     width: Option<DefiniteLength>,
+//     action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+//     secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+//     /// Used to pass down some content to the button
+//     /// to enable creating custom buttons.
+//     children: SmallVec<[AnyElement; 2]>,
+// }
+
+pub trait ButtonCommon: Clickable {
+    fn id(&self) -> &ElementId;
+    fn style(self, style: ButtonStyle2) -> Self;
+    fn disabled(self, disabled: bool) -> Self;
+    fn size(self, size: ButtonSize2) -> Self;
+    fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
+    // fn width(&mut self, width: DefiniteLength) -> &mut Self;
+}
+
+// pub struct LabelButton {
+//     // Base properties...
+//     id: ElementId,
+//     appearance: ButtonAppearance,
+//     state: InteractionState,
+//     disabled: bool,
+//     size: ButtonSize,
+//     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+//     width: Option<DefiniteLength>,
+//     // Button-specific properties...
+//     label: Option<SharedString>,
+//     label_color: Option<Color>,
+//     icon: Option<Icon>,
+//     icon_color: Option<Color>,
+//     icon_position: Option<IconPosition>,
+//     // Define more fields for additional properties as needed
+// }
+
+// impl ButtonCommon for LabelButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+
+//     fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
+//         self.style= style;
+//         self
+//     }
+//     // implement methods from ButtonCommon trait...
+// }
+
+// impl LabelButton {
+//     pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
+//         Self {
+//             id: id.into(),
+//             label: Some(label.into()),
+//             // initialize other fields with default values...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to Button type...
+// }
+
+// TODO: Icon Button
+
+#[derive(IntoElement)]
+pub struct ButtonLike {
+    id: ElementId,
+    style: ButtonStyle2,
+    disabled: bool,
+    size: ButtonSize2,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl ButtonLike {
+    pub fn children(
+        &mut self,
+        children: impl IntoIterator<Item = impl Into<AnyElement>>,
+    ) -> &mut Self {
+        self.children = children.into_iter().map(Into::into).collect();
+        self
+    }
+
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            style: ButtonStyle2::default(),
+            disabled: false,
+            size: ButtonSize2::Default,
+            tooltip: None,
+            children: SmallVec::new(),
+            on_click: None,
+        }
+    }
+}
+
+impl Clickable for ButtonLike {
+    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_click = Some(Box::new(handler));
+        self
+    }
+}
+
+// impl Selectable for ButtonLike {
+//     fn selected(&mut self, selected: bool) -> &mut Self {
+//         todo!()
+//     }
+
+//     fn selected_tooltip(
+//         &mut self,
+//         tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
+//     ) -> &mut Self {
+//         todo!()
+//     }
+// }
+
+impl ButtonCommon for ButtonLike {
+    fn id(&self) -> &ElementId {
+        &self.id
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.style = style;
+        self
+    }
+
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.size = size;
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl RenderOnce for ButtonLike {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        h_stack()
+            .id(self.id.clone())
+            .h(self.size.height())
+            .rounded_md()
+            .cursor_pointer()
+            .gap_1()
+            .px_1()
+            .bg(self.style.enabled(cx).background)
+            .hover(|hover| hover.bg(self.style.hovered(cx).background))
+            .active(|active| active.bg(self.style.active(cx).background))
+            .when_some(
+                self.on_click.filter(|_| !self.disabled),
+                |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)),
+            )
+            .when_some(self.tooltip, |this, tooltip| {
+                this.tooltip(move |cx| tooltip(cx))
+            })
+            .children(self.children)
+    }
+}
+
+impl ParentElement for ButtonLike {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+// pub struct ToggleButton {
+//     // based on either IconButton2 or Button, with additional 'selected: bool' property
+// }
+
+// impl ButtonCommon for ToggleButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+//     // ... Implement other methods from ButtonCommon trait with builder patterns...
+// }
+
+// impl ToggleButton {
+//     pub fn new() -> Self {
+//         // Initialize with default values
+//         Self {
+//             // ... initialize fields, possibly with defaults or required parameters...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to ToggleButton type...
+// }
+
+// pub struct SplitButton {
+//     // Base properties...
+//     id: ElementId,
+//     // Button-specific properties, possibly including a DefaultButton
+//     secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
+//     // More fields as necessary...
+// }
+
+// impl ButtonCommon for SplitButton {
+//     fn id(&self) -> &ElementId {
+//         &self.id
+//     }
+//     // ... Implement other methods from ButtonCommon trait with builder patterns...
+// }
+
+// impl SplitButton {
+//     pub fn new(id: impl Into<ElementId>) -> Self {
+//         Self {
+//             id: id.into(),
+//             // ... initialize other fields with default values...
+//         }
+//     }
+
+//     // ... Define other builder methods specific to SplitButton type...
+// }

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

@@ -8,7 +8,7 @@ pub struct IconButton {
     color: Color,
     size: IconSize,
     variant: ButtonVariant,
-    state: InteractionState,
+    disabled: bool,
     selected: bool,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -18,9 +18,9 @@ impl RenderOnce for IconButton {
     type Rendered = Stateful<Div>;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let icon_color = match (self.state, self.color) {
-            (InteractionState::Disabled, _) => Color::Disabled,
-            (InteractionState::Active, _) => Color::Selected,
+        let icon_color = match (self.disabled, self.selected, self.color) {
+            (true, _, _) => Color::Disabled,
+            (false, true, _) => Color::Selected,
             _ => self.color,
         };
 
@@ -82,8 +82,8 @@ impl IconButton {
             color: Color::default(),
             size: Default::default(),
             variant: ButtonVariant::default(),
-            state: InteractionState::default(),
             selected: false,
+            disabled: false,
             tooltip: None,
             on_click: None,
         }
@@ -109,13 +109,13 @@ impl IconButton {
         self
     }
 
-    pub fn state(mut self, state: InteractionState) -> Self {
-        self.state = state;
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
         self
     }
 
-    pub fn selected(mut self, selected: bool) -> Self {
-        self.selected = selected;
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
         self
     }
 

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

@@ -1,108 +0,0 @@
-use crate::{prelude::*, Label};
-use gpui::{prelude::*, Div, IntoElement, Stateful};
-
-#[derive(Default, PartialEq)]
-pub enum InputVariant {
-    #[default]
-    Ghost,
-    Filled,
-}
-
-#[derive(IntoElement)]
-pub struct Input {
-    placeholder: SharedString,
-    value: String,
-    state: InteractionState,
-    variant: InputVariant,
-    disabled: bool,
-    is_active: bool,
-}
-
-impl RenderOnce for Input {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
-            InputVariant::Ghost => (
-                cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_hover,
-                cx.theme().colors().ghost_element_active,
-            ),
-            InputVariant::Filled => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().element_hover,
-                cx.theme().colors().element_active,
-            ),
-        };
-
-        let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Placeholder
-        });
-
-        let label = Label::new(self.value.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Default
-        });
-
-        div()
-            .id("input")
-            .h_7()
-            .w_full()
-            .px_2()
-            .border()
-            .border_color(cx.theme().styles.system.transparent)
-            .bg(input_bg)
-            .hover(|style| style.bg(input_hover_bg))
-            .active(|style| style.bg(input_active_bg))
-            .flex()
-            .items_center()
-            .child(div().flex().items_center().text_ui_sm().map(move |this| {
-                if self.value.is_empty() {
-                    this.child(placeholder_label)
-                } else {
-                    this.child(label)
-                }
-            }))
-    }
-}
-
-impl Input {
-    pub fn new(placeholder: impl Into<SharedString>) -> Self {
-        Self {
-            placeholder: placeholder.into(),
-            value: "".to_string(),
-            state: InteractionState::default(),
-            variant: InputVariant::default(),
-            disabled: false,
-            is_active: false,
-        }
-    }
-
-    pub fn value(mut self, value: String) -> Self {
-        self.value = value;
-        self
-    }
-
-    pub fn state(mut self, state: InteractionState) -> Self {
-        self.state = state;
-        self
-    }
-
-    pub fn variant(mut self, variant: InputVariant) -> Self {
-        self.variant = variant;
-        self
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn is_active(mut self, is_active: bool) -> Self {
-        self.is_active = is_active;
-        self
-    }
-}

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

@@ -4,18 +4,15 @@ mod checkbox;
 mod context_menu;
 mod icon;
 mod icon_button;
-mod input;
 mod keybinding;
 mod label;
 mod list_item;
-
 pub use avatar::*;
 pub use button::*;
 pub use checkbox::*;
 pub use context_menu::*;
 pub use icon::*;
 pub use icon_button::*;
-pub use input::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list_item::*;

crates/ui2/src/components/stories/button.rs 🔗

@@ -1,9 +1,8 @@
-use gpui::{rems, Div, Render};
+use gpui::{Div, Render};
 use story::Story;
-use strum::IntoEnumIterator;
 
 use crate::prelude::*;
-use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+use crate::{h_stack, Button, Icon, IconPosition};
 
 pub struct ButtonStory;
 
@@ -11,8 +10,6 @@ impl Render for ButtonStory {
     type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        let states = InteractionState::iter();
-
         Story::container()
             .child(Story::title_for::<Button>())
             .child(
@@ -20,121 +17,56 @@ impl Render for ButtonStory {
                     .flex()
                     .gap_8()
                     .child(
-                        div()
-                            .child(Story::label("Ghost (Default)"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Left Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Right Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
-                    )
-                    .child(
-                        div()
-                            .child(Story::label("Filled"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Left Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Right Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
+                        div().child(Story::label("Ghost (Default)")).child(
+                            h_stack()
+                                .gap_2()
+                                .child(Button::new("Label").variant(ButtonVariant::Ghost)),
+                        ),
                     )
+                    .child(Story::label("Ghost – Left Icon"))
                     .child(
-                        div()
-                            .child(Story::label("Fixed With"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("Fixed With – Left Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("Fixed With – Right Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            }))),
+                        h_stack().gap_2().child(
+                            Button::new("Label")
+                                .variant(ButtonVariant::Ghost)
+                                .icon(Icon::Plus)
+                                .icon_position(IconPosition::Left),
+                        ),
                     ),
             )
+            .child(Story::label("Ghost – Right Icon"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Ghost)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Right),
+                ),
+            )
+            .child(
+                div().child(Story::label("Filled")).child(
+                    h_stack()
+                        .gap_2()
+                        .child(Button::new("Label").variant(ButtonVariant::Filled)),
+                ),
+            )
+            .child(Story::label("Filled – Left Button"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Filled)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Left),
+                ),
+            )
+            .child(Story::label("Filled – Right Button"))
+            .child(
+                h_stack().gap_2().child(
+                    Button::new("Label")
+                        .variant(ButtonVariant::Filled)
+                        .icon(Icon::Plus)
+                        .icon_position(IconPosition::Right),
+                ),
+            )
             .child(Story::label("Button with `on_click`"))
             .child(
                 Button::new("Label")

crates/ui2/src/components/stories/input.rs 🔗

@@ -1,18 +0,0 @@
-use gpui::{Div, Render};
-use story::Story;
-
-use crate::prelude::*;
-use crate::Input;
-
-pub struct InputStory;
-
-impl Render for InputStory {
-    type Element = Div;
-
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        Story::container()
-            .child(Story::title_for::<Input>())
-            .child(Story::label("Default"))
-            .child(div().flex().child(Input::new("Search")))
-    }
-}

crates/ui2/src/fixed.rs 🔗

@@ -0,0 +1,6 @@
+use gpui::DefiniteLength;
+
+pub trait FixedWidth {
+    fn width(self, width: DefiniteLength) -> Self;
+    fn full_width(self) -> Self;
+}

crates/ui2/src/prelude.rs 🔗

@@ -3,62 +3,9 @@ pub use gpui::{
     ViewContext, WindowContext,
 };
 
+pub use crate::clickable::*;
+pub use crate::fixed::*;
+pub use crate::selectable::*;
 pub use crate::StyledExt;
 pub use crate::{ButtonVariant, Color};
 pub use theme::ActiveTheme;
-
-use strum::EnumIter;
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum IconSide {
-    #[default]
-    Left,
-    Right,
-}
-
-#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
-pub enum InteractionState {
-    /// An element that is enabled and not hovered, active, focused, or disabled.
-    ///
-    /// This is often referred to as the "default" state.
-    #[default]
-    Enabled,
-    /// An element that is hovered.
-    Hovered,
-    /// An element has an active mouse down or touch start event on it.
-    Active,
-    /// An element that is focused using the keyboard.
-    Focused,
-    /// An element that is disabled.
-    Disabled,
-    /// A toggleable element that is selected, like the active button in a
-    /// button toggle group.
-    Selected,
-}
-
-impl InteractionState {
-    pub fn if_enabled(&self, enabled: bool) -> Self {
-        if enabled {
-            *self
-        } else {
-            InteractionState::Disabled
-        }
-    }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum Selection {
-    #[default]
-    Unselected,
-    Indeterminate,
-    Selected,
-}
-
-impl Selection {
-    pub fn inverse(&self) -> Self {
-        match self {
-            Self::Unselected | Self::Indeterminate => Self::Selected,
-            Self::Selected => Self::Unselected,
-        }
-    }
-}

crates/ui2/src/selectable.rs 🔗

@@ -0,0 +1,26 @@
+use gpui::{AnyView, WindowContext};
+
+pub trait Selectable {
+    fn selected(self, selected: bool) -> Self;
+    fn selected_tooltip(
+        self,
+        tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
+    ) -> Self;
+}
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum Selection {
+    #[default]
+    Unselected,
+    Indeterminate,
+    Selected,
+}
+
+impl Selection {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::Unselected | Self::Indeterminate => Self::Selected,
+            Self::Selected => Self::Unselected,
+        }
+    }
+}

crates/ui2/src/styles/color.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{Hsla, WindowContext};
 use theme::ActiveTheme;
 
-#[derive(Default, PartialEq, Copy, Clone)]
+#[derive(Debug, Default, PartialEq, Copy, Clone)]
 pub enum Color {
     #[default]
     Default,

crates/ui2/src/ui2.rs 🔗

@@ -12,13 +12,19 @@
 #![doc = include_str!("../docs/building-ui.md")]
 #![doc = include_str!("../docs/todo.md")]
 
+mod clickable;
 mod components;
+mod fixed;
 pub mod prelude;
+mod selectable;
 mod styled_ext;
 mod styles;
 pub mod utils;
 
+pub use clickable::*;
 pub use components::*;
+pub use fixed::*;
 pub use prelude::*;
+pub use selectable::*;
 pub use styled_ext::*;
 pub use styles::*;

crates/workspace2/src/dock.rs 🔗

@@ -7,8 +7,8 @@ use gpui::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use theme2::ActiveTheme;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
+use ui::prelude::*;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -686,22 +686,26 @@ impl Render for PanelButtons {
                 let name = entry.panel.persistent_name();
                 let panel = entry.panel.clone();
 
-                let mut button: IconButton = if i == active_index && is_open {
+                let is_active_button = i == active_index && is_open;
+
+                let (action, tooltip) = if is_active_button {
                     let action = dock.toggle_action();
+
                     let tooltip: SharedString =
                         format!("Close {} dock", dock.position.to_label()).into();
-                    IconButton::new(name, icon)
-                        .state(InteractionState::Active)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+
+                    (action, tooltip)
                 } else {
                     let action = entry.panel.toggle_action(cx);
 
-                    IconButton::new(name, icon)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
+                    (action, name.into())
                 };
 
+                let button = IconButton::new(name, icon)
+                    .selected(is_active_button)
+                    .action(action.boxed_clone())
+                    .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx));
+
                 Some(
                     menu_handle(name)
                         .menu(move |cx| {

crates/workspace2/src/pane.rs 🔗

@@ -1482,18 +1482,14 @@ impl Pane {
                             .gap_px()
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_backward", Icon::ArrowLeft).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_backward()),
-                                    ),
+                                    IconButton::new("navigate_backward", Icon::ArrowLeft)
+                                        .disabled(!self.can_navigate_backward()),
                                 ),
                             )
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_forward", Icon::ArrowRight).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_forward()),
-                                    ),
+                                    IconButton::new("navigate_forward", Icon::ArrowRight)
+                                        .disabled(!self.can_navigate_forward()),
                                 ),
                             ),
                     ),

crates/workspace2/src/status_bar.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{h_stack, Button, Icon, IconButton};
 use util::ResultExt;
 

crates/workspace2/src/toolbar.rs 🔗

@@ -3,7 +3,7 @@ use gpui::{
     div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
     ViewContext, WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
 
 pub enum ToolbarItemEvent {