Merge branch 'main' into collab-panel2

Conrad Irwin created

Change summary

crates/collab_ui2/src/collab_panel.rs                             |  39 
crates/collab_ui2/src/collab_titlebar_item.rs                     |  46 
crates/collab_ui2/src/notifications/incoming_call_notification.rs |  31 
crates/diagnostics2/src/toolbar_controls.rs                       |   1 
crates/editor2/src/editor.rs                                      |   5 
crates/editor2/src/element.rs                                     |   1 
crates/search2/src/buffer_search.rs                               |  14 
crates/search2/src/search.rs                                      |  19 
crates/search2/src/search_bar.rs                                  |  13 
crates/ui2/src/components.rs                                      |   4 
crates/ui2/src/components/button.rs                               | 228 -
crates/ui2/src/components/button/button.rs                        |  91 
crates/ui2/src/components/button/button_like.rs                   | 169 
crates/ui2/src/components/button/icon_button.rs                   | 102 
crates/ui2/src/components/button/mod.rs                           |   7 
crates/ui2/src/components/disclosure.rs                           |   4 
crates/ui2/src/components/icon_button.rs                          | 135 
crates/ui2/src/components/list/list_header.rs                     |   2 
crates/ui2/src/components/stories/button.rs                       |  69 
crates/ui2/src/components/stories/icon_button.rs                  |   6 
crates/ui2/src/prelude.rs                                         |   3 
crates/workspace2/src/dock.rs                                     |  14 
crates/workspace2/src/notifications.rs                            |  15 
crates/workspace2/src/status_bar.rs                               |   4 
crates/workspace2/src/toolbar.rs                                  |   4 
25 files changed, 338 insertions(+), 688 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -178,6 +178,7 @@ use gpui::{
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
+use ui::prelude::*;
 use ui::{
     h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
     Label, List, ListHeader, ListItem, Tooltip,
@@ -2333,18 +2334,20 @@ impl CollabPanel {
     }
 
     fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener(
-            |this, _, cx| {
-                let client = this.client.clone();
-                cx.spawn(|_, mut cx| async move {
-                    client
-                        .authenticate_and_connect(true, &cx)
-                        .await
-                        .notify_async_err(&mut cx);
-                })
-                .detach()
-            },
-        )))
+        v_stack().child(
+            Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
+                |this, _, cx| {
+                    let client = this.client.clone();
+                    cx.spawn(|_, mut cx| async move {
+                        client
+                            .authenticate_and_connect(true, &cx)
+                            .await
+                            .notify_async_err(&mut cx);
+                    })
+                    .detach()
+                },
+            )),
+        )
     }
 
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List {
@@ -2559,7 +2562,7 @@ impl CollabPanel {
                                 .group_hover("", |style| style.visible())
                                 .child(
                                     IconButton::new("remove_contact", Icon::Close)
-                                        .color(Color::Muted)
+                                        .icon_color(Color::Muted)
                                         .tooltip(|cx| Tooltip::text("Remove Contact", cx))
                                         .on_click(cx.listener({
                                             let github_login = github_login.clone();
@@ -2623,13 +2626,13 @@ impl CollabPanel {
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, false, cx);
                     }))
-                    .color(color)
+                    .icon_color(color)
                     .tooltip(|cx| Tooltip::text("Decline invite", cx)),
                 IconButton::new("remove_contact", Icon::Check)
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, true, cx);
                     }))
-                    .color(color)
+                    .icon_color(color)
                     .tooltip(|cx| Tooltip::text("Accept invite", cx)),
             ]
         } else {
@@ -2638,7 +2641,7 @@ impl CollabPanel {
                 .on_click(cx.listener(move |this, _, cx| {
                     this.remove_contact(user_id, &github_login, cx);
                 }))
-                .color(color)
+                .icon_color(color)
                 .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
         };
 
@@ -2780,7 +2783,7 @@ impl CollabPanel {
                                                     "channel_chat",
                                                     Icon::MessageBubbles,
                                                 )
-                                                .color(if has_messages_notification {
+                                                .icon_color(if has_messages_notification {
                                                     Color::Default
                                                 } else {
                                                     Color::Muted
@@ -2795,7 +2798,7 @@ impl CollabPanel {
                                             .group_hover("", |style| style.visible())
                                             .child(
                                                 IconButton::new("channel_notes", Icon::File)
-                                                    .color(if has_notes_notification {
+                                                    .icon_color(if has_notes_notification {
                                                         Color::Default
                                                     } else {
                                                         Color::Muted

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -37,10 +37,7 @@ use gpui::{
 };
 use project::Project;
 use theme::ActiveTheme;
-use ui::{
-    h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
-    IconElement, IconSize, KeyBinding, Tooltip,
-};
+use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip};
 use util::ResultExt;
 use workspace::{notifications::NotifyResultExt, Workspace};
 
@@ -156,8 +153,8 @@ impl Render for CollabTitlebarItem {
                             .border_color(gpui::red())
                             .id("project_owner_indicator")
                             .child(
-                                Button::new("player")
-                                    .variant(ButtonVariant::Ghost)
+                                Button::new("player", "player")
+                                    .style(ButtonStyle2::Subtle)
                                     .color(Some(Color::Player(0))),
                             )
                             .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
@@ -168,7 +165,10 @@ impl Render for CollabTitlebarItem {
                             .border()
                             .border_color(gpui::red())
                             .id("titlebar_project_menu_button")
-                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .child(
+                                Button::new("project_name", "project_name")
+                                    .style(ButtonStyle2::Subtle),
+                            )
                             .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
                     )
                     // TODO - Add git menu
@@ -178,8 +178,8 @@ impl Render for CollabTitlebarItem {
                             .border_color(gpui::red())
                             .id("titlebar_git_menu_button")
                             .child(
-                                Button::new("branch_name")
-                                    .variant(ButtonVariant::Ghost)
+                                Button::new("branch_name", "branch_name")
+                                    .style(ButtonStyle2::Subtle)
                                     .color(Some(Color::Muted)),
                             )
                             .tooltip(move |cx| {
@@ -238,7 +238,10 @@ impl Render for CollabTitlebarItem {
                     h_stack()
                         .child(
                             h_stack()
-                                .child(Button::new(if is_shared { "Unshare" } else { "Share" }))
+                                .child(Button::new(
+                                    "toggle_sharing",
+                                    if is_shared { "Unshare" } else { "Share" },
+                                ))
                                 .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
                                     let workspace = workspace.clone();
                                     move |_, cx| {
@@ -291,7 +294,7 @@ impl Render for CollabTitlebarItem {
                         this.child(ui::Avatar::data(avatar))
                     })
                 } else {
-                    this.child(Button::new("Sign in").on_click(move |_, cx| {
+                    this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
                         let client = client.clone();
                         cx.spawn(move |mut cx| async move {
                             client
@@ -301,27 +304,6 @@ 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/collab_ui2/src/notifications/incoming_call_notification.rs 🔗

@@ -2,10 +2,11 @@ use crate::notification_window_options;
 use call::{ActiveCall, IncomingCall};
 use futures::StreamExt;
 use gpui::{
-    div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
-    StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
+    div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
+    VisualContext as _, WindowHandle,
 };
 use std::sync::{Arc, Weak};
+use ui::prelude::*;
 use ui::{h_stack, v_stack, Avatar, Button, Label};
 use util::ResultExt;
 use workspace::AppState;
@@ -199,14 +200,24 @@ impl IncomingCallNotification {
 
     fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
         h_stack()
-            .child(Button::new("Accept").render(cx).bg(green()).on_click({
-                let state = self.state.clone();
-                move |_, cx| state.respond(true, cx)
-            }))
-            .child(Button::new("Decline").render(cx).bg(red()).on_click({
-                let state = self.state.clone();
-                move |_, cx| state.respond(false, cx)
-            }))
+            .child(
+                Button::new("accept", "Accept")
+                    .render(cx)
+                    // .bg(green())
+                    .on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(true, cx)
+                    }),
+            )
+            .child(
+                Button::new("decline", "Decline")
+                    .render(cx)
+                    // .bg(red())
+                    .on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(false, cx)
+                    }),
+            )
 
         // enum Accept {}
         // enum Decline {}

crates/diagnostics2/src/toolbar_controls.rs 🔗

@@ -1,5 +1,6 @@
 use crate::ProjectDiagnosticsEditor;
 use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
+use ui::prelude::*;
 use ui::{Icon, IconButton, Tooltip};
 use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 

crates/editor2/src/editor.rs 🔗

@@ -99,7 +99,8 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip};
+use ui::prelude::*;
+use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
@@ -4391,7 +4392,7 @@ impl Editor {
                                         editor.fold_at(&FoldAt { buffer_row }, cx);
                                     }
                                 }))
-                                .color(ui::Color::Muted)
+                                .icon_color(ui::Color::Muted)
                         })
                     })
                     .flatten()

crates/editor2/src/element.rs 🔗

@@ -48,6 +48,7 @@ use std::{
 };
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
+use ui::prelude::*;
 use ui::{h_stack, IconButton, Tooltip};
 use util::ResultExt;
 use workspace::item::Item;

crates/search2/src/buffer_search.rs 🔗

@@ -18,7 +18,7 @@ use project::search::SearchQuery;
 use serde::Deserialize;
 use std::{any::Any, sync::Arc};
 
-use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
+use ui::{h_stack, Icon, IconButton, IconElement};
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
@@ -214,10 +214,11 @@ impl Render for BufferSearchBar {
             .child(
                 h_stack()
                     .flex_none()
-                    .child(ButtonGroup::new(vec![
-                        search_button_for_mode(SearchMode::Text),
-                        search_button_for_mode(SearchMode::Regex),
-                    ]))
+                    .child(
+                        h_stack()
+                            .child(search_button_for_mode(SearchMode::Text))
+                            .child(search_button_for_mode(SearchMode::Regex)),
+                    )
                     .when(supported_options.replacement, |this| {
                         this.child(super::toggle_replace_button(self.replace_enabled))
                     }),
@@ -586,8 +587,7 @@ impl BufferSearchBar {
 
         // let style = theme.search.action_button.clone();
 
-        IconButton::new(0, ui::Icon::SelectAll)
-            .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches)))
+        IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
     }
 
     pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {

crates/search2/src/search.rs 🔗

@@ -3,7 +3,8 @@ pub use buffer_search::BufferSearchBar;
 use gpui::{actions, Action, AppContext, IntoElement};
 pub use mode::SearchMode;
 use project::search::SearchQuery;
-use ui::ButtonVariant;
+use ui::prelude::*;
+use ui::{ButtonStyle2, Icon, IconButton};
 //pub use project_search::{ProjectSearchBar, ProjectSearchView};
 // use theme::components::{
 //     action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
@@ -83,35 +84,35 @@ impl SearchOptions {
     }
 
     pub fn as_button(&self, active: bool) -> impl IntoElement {
-        ui::IconButton::new(0, self.icon())
+        IconButton::new(0, self.icon())
             .on_click({
                 let action = self.to_toggle_action();
                 move |_, cx| {
                     cx.dispatch_action(action.boxed_clone());
                 }
             })
-            .variant(ui::ButtonVariant::Ghost)
-            .when(active, |button| button.variant(ButtonVariant::Filled))
+            .style(ButtonStyle2::Subtle)
+            .when(active, |button| button.style(ButtonStyle2::Filled))
     }
 }
 
 fn toggle_replace_button(active: bool) -> impl IntoElement {
     // todo: add toggle_replace button
-    ui::IconButton::new(0, ui::Icon::Replace)
+    IconButton::new(0, Icon::Replace)
         .on_click(|_, cx| {
             cx.dispatch_action(Box::new(ToggleReplace));
             cx.notify();
         })
-        .variant(ui::ButtonVariant::Ghost)
-        .when(active, |button| button.variant(ButtonVariant::Filled))
+        .style(ButtonStyle2::Subtle)
+        .when(active, |button| button.style(ButtonStyle2::Filled))
 }
 
 fn render_replace_button(
     action: impl Action + 'static + Send + Sync,
-    icon: ui::Icon,
+    icon: Icon,
 ) -> impl IntoElement {
     // todo: add tooltip
-    ui::IconButton::new(0, icon).on_click(move |_, cx| {
+    IconButton::new(0, icon).on_click(move |_, cx| {
         cx.dispatch_action(action.boxed_clone());
     })
 }

crates/search2/src/search_bar.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{ClickEvent, IntoElement, WindowContext};
-use ui::{Button, ButtonVariant, IconButton};
+use ui::prelude::*;
+use ui::{Button, IconButton};
 
 use crate::mode::SearchMode;
 
@@ -23,13 +24,7 @@ pub(crate) fn render_search_mode_button(
     is_active: bool,
     on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 ) -> Button {
-    let button_variant = if is_active {
-        ButtonVariant::Filled
-    } else {
-        ButtonVariant::Ghost
-    };
-
-    Button::new(mode.label())
+    Button::new(mode.label(), mode.label())
+        .selected(is_active)
         .on_click(on_click)
-        .variant(button_variant)
 }

crates/ui2/src/components.rs 🔗

@@ -1,12 +1,10 @@
 mod avatar;
 mod button;
-mod button2;
 mod checkbox;
 mod context_menu;
 mod disclosure;
 mod divider;
 mod icon;
-mod icon_button;
 mod keybinding;
 mod label;
 mod list;
@@ -19,13 +17,11 @@ 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 keybinding::*;
 pub use label::*;
 pub use list::*;

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

@@ -1,228 +0,0 @@
-use gpui::{
-    ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
-};
-use std::rc::Rc;
-
-use crate::prelude::*;
-use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
-
-/// Provides the flexibility to use either a standard
-/// button or an icon button in a given context.
-pub enum ButtonOrIconButton {
-    Button(Button),
-    IconButton(IconButton),
-}
-
-impl From<Button> for ButtonOrIconButton {
-    fn from(value: Button) -> Self {
-        Self::Button(value)
-    }
-}
-
-impl From<IconButton> for ButtonOrIconButton {
-    fn from(value: IconButton) -> Self {
-        Self::IconButton(value)
-    }
-}
-
-#[derive(Default, PartialEq, Clone, Copy)]
-pub enum IconPosition {
-    #[default]
-    Left,
-    Right,
-}
-
-#[derive(Default, Copy, Clone, PartialEq)]
-pub enum ButtonVariant {
-    #[default]
-    Ghost,
-    Filled,
-}
-
-impl ButtonVariant {
-    pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
-            ButtonVariant::Filled => cx.theme().colors().element_background,
-        }
-    }
-
-    pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
-            ButtonVariant::Filled => cx.theme().colors().element_hover,
-        }
-    }
-
-    pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
-            ButtonVariant::Filled => cx.theme().colors().element_active,
-        }
-    }
-}
-
-#[derive(IntoElement)]
-pub struct Button {
-    disabled: bool,
-    click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
-    icon: Option<Icon>,
-    icon_position: Option<IconPosition>,
-    label: SharedString,
-    variant: ButtonVariant,
-    width: Option<DefiniteLength>,
-    color: Option<Color>,
-}
-
-impl RenderOnce for Button {
-    type Rendered = gpui::Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let (icon_color, label_color) = match (self.disabled, self.color) {
-            (true, _) => (Color::Disabled, Color::Disabled),
-            (_, None) => (Color::Default, Color::Default),
-            (_, Some(color)) => (Color::from(color), color),
-        };
-
-        let mut button = h_stack()
-            .id(SharedString::from(format!("{}", self.label)))
-            .relative()
-            .p_1()
-            .text_ui()
-            .rounded_md()
-            .bg(self.variant.bg_color(cx))
-            .cursor_pointer()
-            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
-            .active(|style| style.bg(self.variant.bg_color_active(cx)));
-
-        match (self.icon, self.icon_position) {
-            (Some(_), Some(IconPosition::Left)) => {
-                button = button
-                    .gap_1()
-                    .child(self.render_label(label_color))
-                    .children(self.render_icon(icon_color))
-            }
-            (Some(_), Some(IconPosition::Right)) => {
-                button = button
-                    .gap_1()
-                    .children(self.render_icon(icon_color))
-                    .child(self.render_label(label_color))
-            }
-            (_, _) => button = button.child(self.render_label(label_color)),
-        }
-
-        if let Some(width) = self.width {
-            button = button.w(width).justify_center();
-        }
-
-        if let Some(click_handler) = self.click_handler.clone() {
-            button = button.on_click(move |event, cx| {
-                click_handler(event, cx);
-            });
-        }
-
-        button
-    }
-}
-
-impl Button {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            disabled: false,
-            click_handler: None,
-            icon: None,
-            icon_position: None,
-            label: label.into(),
-            variant: Default::default(),
-            width: Default::default(),
-            color: None,
-        }
-    }
-
-    pub fn ghost(label: impl Into<SharedString>) -> Self {
-        Self::new(label).variant(ButtonVariant::Ghost)
-    }
-
-    pub fn variant(mut self, variant: ButtonVariant) -> Self {
-        self.variant = variant;
-        self
-    }
-
-    pub fn icon(mut self, icon: Icon) -> Self {
-        self.icon = Some(icon);
-        self
-    }
-
-    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
-        if self.icon.is_none() {
-            panic!("An icon must be present if an icon_position is provided.");
-        }
-        self.icon_position = Some(icon_position);
-        self
-    }
-
-    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
-        self.width = width;
-        self
-    }
-
-    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
-        self.click_handler = Some(Rc::new(handler));
-        self
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn color(mut self, color: Option<Color>) -> Self {
-        self.color = color;
-        self
-    }
-
-    pub fn label_color(&self, color: Option<Color>) -> Color {
-        if self.disabled {
-            Color::Disabled
-        } else if let Some(color) = color {
-            color
-        } else {
-            Default::default()
-        }
-    }
-
-    fn render_label(&self, color: Color) -> Label {
-        Label::new(self.label.clone())
-            .color(color)
-            .line_height_style(LineHeightStyle::UILabel)
-    }
-
-    fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
-        self.icon.map(|i| IconElement::new(i).color(icon_color))
-    }
-}
-
-#[derive(IntoElement)]
-pub struct ButtonGroup {
-    buttons: Vec<Button>,
-}
-
-impl RenderOnce for ButtonGroup {
-    type Rendered = Div;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let mut group = h_stack();
-
-        for button in self.buttons.into_iter() {
-            group = group.child(button.render(cx));
-        }
-
-        group
-    }
-}
-
-impl ButtonGroup {
-    pub fn new(buttons: Vec<Button>) -> Self {
-        Self { buttons }
-    }
-}

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

@@ -0,0 +1,91 @@
+use gpui::AnyView;
+
+use crate::prelude::*;
+use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
+
+#[derive(IntoElement)]
+pub struct Button {
+    base: ButtonLike,
+    label: SharedString,
+    label_color: Option<Color>,
+    selected: bool,
+}
+
+impl Button {
+    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
+        Self {
+            base: ButtonLike::new(id),
+            label: label.into(),
+            label_color: None,
+            selected: false,
+        }
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
+    pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
+        self.label_color = label_color.into();
+        self
+    }
+}
+
+impl Clickable for Button {
+    fn on_click(
+        mut self,
+        handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.base = self.base.on_click(handler);
+        self
+    }
+}
+
+impl Disableable for Button {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.base = self.base.disabled(disabled);
+        self
+    }
+}
+
+impl ButtonCommon for Button {
+    fn id(&self) -> &ElementId {
+        self.base.id()
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.base = self.base.style(style);
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.base = self.base.size(size);
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.base = self.base.tooltip(tooltip);
+        self
+    }
+}
+
+impl RenderOnce for Button {
+    type Rendered = ButtonLike;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let label_color = if self.base.disabled {
+            Color::Disabled
+        } else if self.selected {
+            Color::Selected
+        } else {
+            Color::Default
+        };
+
+        self.base.child(
+            Label::new(self.label)
+                .color(label_color)
+                .line_height_style(LineHeightStyle::UILabel),
+        )
+    }
+}

crates/ui2/src/components/button2.rs → crates/ui2/src/components/button/button_like.rs 🔗

@@ -1,31 +1,17 @@
-use gpui::{
-    rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
-    StatefulInteractiveElement, WindowContext,
-};
+use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
 use smallvec::SmallVec;
 
-use crate::{h_stack, prelude::*};
+use crate::h_stack;
+use crate::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,
+pub trait ButtonCommon: Clickable + Disableable {
+    fn id(&self) -> &ElementId;
+    fn style(self, style: ButtonStyle2) -> Self;
+    fn size(self, size: ButtonSize2) -> Self;
+    fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
 }
 
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum ButtonStyle2 {
     #[default]
     Filled,
@@ -34,7 +20,7 @@ pub enum ButtonStyle2 {
     Transparent,
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
 pub struct ButtonStyle {
     pub background: Hsla,
     pub border_color: Hsla,
@@ -181,82 +167,11 @@ impl ButtonSize2 {
     }
 }
 
-// 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 + Disableable {
-    fn id(&self) -> &ElementId;
-    fn style(self, style: ButtonStyle2) -> Self;
-    fn size(self, size: ButtonSize2) -> Self;
-    fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> 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,
+    pub(super) style: ButtonStyle2,
+    pub(super) disabled: bool,
     size: ButtonSize2,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -325,6 +240,12 @@ impl ButtonCommon for ButtonLike {
     }
 }
 
+impl ParentElement for ButtonLike {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
 impl RenderOnce for ButtonLike {
     type Rendered = Stateful<Div>;
 
@@ -349,57 +270,3 @@ impl RenderOnce for ButtonLike {
             .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/button/icon_button.rs 🔗

@@ -0,0 +1,102 @@
+use gpui::{Action, AnyView};
+
+use crate::prelude::*;
+use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
+
+#[derive(IntoElement)]
+pub struct IconButton {
+    base: ButtonLike,
+    icon: Icon,
+    icon_size: IconSize,
+    icon_color: Color,
+    selected: bool,
+}
+
+impl IconButton {
+    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+        Self {
+            base: ButtonLike::new(id),
+            icon,
+            icon_size: IconSize::default(),
+            icon_color: Color::Default,
+            selected: false,
+        }
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
+    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
+        self.icon_size = icon_size;
+        self
+    }
+
+    pub fn icon_color(mut self, icon_color: Color) -> Self {
+        self.icon_color = icon_color;
+        self
+    }
+
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+}
+
+impl Clickable for IconButton {
+    fn on_click(
+        mut self,
+        handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.base = self.base.on_click(handler);
+        self
+    }
+}
+
+impl Disableable for IconButton {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.base = self.base.disabled(disabled);
+        self
+    }
+}
+
+impl ButtonCommon for IconButton {
+    fn id(&self) -> &ElementId {
+        self.base.id()
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.base = self.base.style(style);
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.base = self.base.size(size);
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.base = self.base.tooltip(tooltip);
+        self
+    }
+}
+
+impl RenderOnce for IconButton {
+    type Rendered = ButtonLike;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let icon_color = if self.base.disabled {
+            Color::Disabled
+        } else if self.selected {
+            Color::Selected
+        } else {
+            self.icon_color
+        };
+
+        self.base.child(
+            IconElement::new(self.icon)
+                .size(self.icon_size)
+                .color(icon_color),
+        )
+    }
+}

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

@@ -39,8 +39,8 @@ impl RenderOnce for Disclosure {
                 false => Icon::ChevronRight,
             },
         )
-        .color(Color::Muted)
-        .size(IconSize::Small)
+        .icon_color(Color::Muted)
+        .icon_size(IconSize::Small)
         .when_some(self.on_toggle, move |this, on_toggle| {
             this.on_click(move |event, cx| on_toggle(event, cx))
         })

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

@@ -1,135 +0,0 @@
-use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
-use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
-
-#[derive(IntoElement)]
-pub struct IconButton {
-    id: ElementId,
-    icon: Icon,
-    color: Color,
-    size: IconSize,
-    variant: ButtonVariant,
-    disabled: bool,
-    selected: bool,
-    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
-    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
-}
-
-impl RenderOnce for IconButton {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let icon_color = match (self.disabled, self.selected, self.color) {
-            (true, _, _) => Color::Disabled,
-            (false, true, _) => Color::Selected,
-            _ => self.color,
-        };
-
-        let (mut bg_color, bg_active_color) = match self.variant {
-            ButtonVariant::Filled => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().element_active,
-            ),
-            ButtonVariant::Ghost => (
-                cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_active,
-            ),
-        };
-
-        if self.selected {
-            bg_color = cx.theme().colors().element_selected;
-        }
-
-        let mut button = h_stack()
-            .id(self.id.clone())
-            .justify_center()
-            .rounded_md()
-            .p_1()
-            .bg(bg_color)
-            .cursor_pointer()
-            // Nate: Trying to figure out the right places we want to show a
-            // hover state here. I think it is a bit heavy to have it on every
-            // place we use an icon button.
-            // .hover(|style| style.bg(bg_hover_color))
-            .active(|style| style.bg(bg_active_color))
-            .child(
-                IconElement::new(self.icon)
-                    .size(self.size)
-                    .color(icon_color),
-            );
-
-        if let Some(click_handler) = self.on_click {
-            button = button.on_click(move |event, cx| {
-                cx.stop_propagation();
-                click_handler(event, cx);
-            })
-        }
-
-        if let Some(tooltip) = self.tooltip {
-            if !self.selected {
-                button = button.tooltip(move |cx| tooltip(cx))
-            }
-        }
-
-        button
-    }
-}
-
-impl IconButton {
-    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
-        Self {
-            id: id.into(),
-            icon,
-            color: Color::default(),
-            size: Default::default(),
-            variant: ButtonVariant::default(),
-            selected: false,
-            disabled: false,
-            tooltip: None,
-            on_click: None,
-        }
-    }
-
-    pub fn icon(mut self, icon: Icon) -> Self {
-        self.icon = icon;
-        self
-    }
-
-    pub fn color(mut self, color: Color) -> Self {
-        self.color = color;
-        self
-    }
-
-    pub fn size(mut self, size: IconSize) -> Self {
-        self.size = size;
-        self
-    }
-
-    pub fn variant(mut self, variant: ButtonVariant) -> Self {
-        self.variant = variant;
-        self
-    }
-
-    pub fn selected(mut self, selected: bool) -> Self {
-        self.selected = selected;
-        self
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
-        self.tooltip = Some(Box::new(tooltip));
-        self
-    }
-
-    pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
-        self.on_click = Some(Box::new(handler));
-        self
-    }
-
-    pub fn action(self, action: Box<dyn Action>) -> Self {
-        self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
-    }
-}

crates/ui2/src/components/list/list_header.rs 🔗

@@ -78,7 +78,7 @@ impl RenderOnce for ListHeader {
                 h_stack()
                     .gap_2()
                     .items_center()
-                    .children(icons.into_iter().map(|i| i.color(Color::Muted))),
+                    .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
             ),
             Some(ListHeaderMeta::Button(label)) => div().child(label),
             Some(ListHeaderMeta::Text(label)) => div().child(label),

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

@@ -2,7 +2,7 @@ use gpui::{Div, Render};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{h_stack, Button, Icon, IconPosition};
+use crate::{Button, ButtonStyle2};
 
 pub struct ButtonStory;
 
@@ -12,66 +12,11 @@ impl Render for ButtonStory {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Button>())
-            .child(
-                div()
-                    .flex()
-                    .gap_8()
-                    .child(
-                        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(
-                        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")
-                    .variant(ButtonVariant::Ghost)
-                    .on_click(|_, _cx| println!("Button clicked.")),
-            )
+            .child(Story::label("Default"))
+            .child(Button::new("default_filled", "Click me"))
+            .child(Story::label("Default (Subtle)"))
+            .child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
+            .child(Story::label("Default (Transparent)"))
+            .child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
     }
 }

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

@@ -14,6 +14,12 @@ impl Render for IconButtonStory {
             .child(Story::title_for::<IconButton>())
             .child(Story::label("Default"))
             .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+            .child(Story::label("Selected"))
+            .child(
+                div()
+                    .w_8()
+                    .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
+            )
             .child(Story::label("With `on_click`"))
             .child(
                 div()

crates/ui2/src/prelude.rs 🔗

@@ -8,6 +8,5 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
-pub use crate::StyledExt;
-pub use crate::{ButtonVariant, Color};
+pub use crate::{ButtonCommon, Color, StyledExt};
 pub use theme::ActiveTheme;

crates/workspace2/src/dock.rs 🔗

@@ -701,11 +701,6 @@ impl Render for PanelButtons {
                     (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| {
@@ -731,7 +726,14 @@ impl Render for PanelButtons {
                         })
                         .anchor(menu_anchor)
                         .attach(menu_attach)
-                        .child(|is_open| button.selected(is_open)),
+                        .child(move |_is_open| {
+                            IconButton::new(name, icon)
+                                .selected(is_active_button)
+                                .action(action.boxed_clone())
+                                .tooltip(move |cx| {
+                                    Tooltip::for_action(tooltip.clone(), &*action, cx)
+                                })
+                        }),
                 )
             });
 

crates/workspace2/src/notifications.rs 🔗

@@ -181,6 +181,7 @@ pub mod simple_message_notification {
     };
     use serde::Deserialize;
     use std::{borrow::Cow, sync::Arc};
+    use ui::prelude::*;
     use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
@@ -287,12 +288,14 @@ pub mod simple_message_notification {
                         ),
                 )
                 .children(self.click_message.iter().map(|message| {
-                    Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
-                        if let Some(on_click) = this.on_click.as_ref() {
-                            (on_click)(cx)
-                        };
-                        this.dismiss(cx)
-                    }))
+                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
+                        |this, _, cx| {
+                            if let Some(on_click) = this.on_click.as_ref() {
+                                (on_click)(cx)
+                            };
+                            this.dismiss(cx)
+                        },
+                    ))
                 }))
         }
     }

crates/workspace2/src/status_bar.rs 🔗

@@ -71,14 +71,14 @@ impl Render for StatusBar {
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(Button::new("15:22")),
+                                    .child(Button::new("status_line_column_numbers", "15:22")),
                             )
                             .child(
                                 // TODO: Language picker
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(Button::new("Rust")),
+                                    .child(Button::new("status_buffer_language", "Rust")),
                             ),
                     )
                     .child(

crates/workspace2/src/toolbar.rs 🔗

@@ -94,9 +94,9 @@ impl Render for Toolbar {
                             .border()
                             .border_color(gpui::red())
                             .p_1()
-                            .child(Button::new("crates"))
+                            .child(Button::new("breadcrumb_crates", "crates"))
                             .child(Label::new("/").color(Color::Muted))
-                            .child(Button::new("workspace2")),
+                            .child(Button::new("breadcrumb_workspace2", "workspace2")),
                     )
                     // Toolbar right side
                     .child(