ui: Refactor the `Button` component icon methods (#51496)

Danilo Leal created

Previously, if you wanted to have a button that contains icons on both
edges, you'd need to use a `ButtonLike` component, which takes any
children. Meanwhile, the `Button` would only take one icon, where you
could control its position through the `IconPosition` enum. This has
always felt unnecessarily limiting. So, this PR removes this limitation
by adding two new methods to the button: `start_icon` and `end_icon`.

In the meantime, I have also been bothered by the unnecessary
indirection in the `IconButton` due to the existence of the `ButtonIcon`
component. So I figured I could also completely eliminate that by adding
some of its methods directly to the `IconButton` and in the Button, just
using a regular `Icon` component.

---
## Before
```rust
Button::new("id", "Label")
    .icon(IconName::Plus)
    .icon_position(IconPosition::Start)
    .icon_size(IconSize::Small)
    .icon_color(Color::Muted)
```

## After
```rust
Button::new("id", "Label")
    .start_icon(Icon::new(IconName::Check))
    .end_icon(Icon::new(IconName::ChevronDown).size(IconSize::XSmall))
```

This should have no visual impact to the UI.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_configuration.rs                                |  45 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs         |  18 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |   8 
crates/agent_ui/src/agent_diff.rs                                         |   9 
crates/agent_ui/src/agent_model_selector.rs                               |  17 
crates/agent_ui/src/agent_panel.rs                                        |  51 
crates/agent_ui/src/agent_registry_ui.rs                                  |  17 
crates/agent_ui/src/config_options.rs                                     |   5 
crates/agent_ui/src/connection_view/thread_view.rs                        |  89 
crates/agent_ui/src/inline_prompt_editor.rs                               |   8 
crates/agent_ui/src/message_editor.rs                                     |   8 
crates/agent_ui/src/mode_selector.rs                                      |   5 
crates/agent_ui/src/model_selector_popover.rs                             |  17 
crates/agent_ui/src/profile_selector.rs                                   |   8 
crates/agent_ui/src/sidebar.rs                                            |  18 
crates/agent_ui/src/text_thread_editor.rs                                 |  29 
crates/agent_ui/src/ui/acp_onboarding_modal.rs                            |   9 
crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs                   |   9 
crates/collab_ui/src/collab_panel.rs                                      |   6 
crates/collab_ui/src/notification_panel.rs                                |   4 
crates/copilot_ui/src/sign_in.rs                                          |  27 
crates/debugger_ui/src/debugger_panel.rs                                  |  36 
crates/edit_prediction_ui/src/rate_prediction_modal.rs                    |   8 
crates/extensions_ui/src/extensions_ui.rs                                 |  31 
crates/git_graph/src/git_graph.rs                                         |  21 
crates/git_ui/src/blame_ui.rs                                             |  18 
crates/git_ui/src/commit_modal.rs                                         |   9 
crates/git_ui/src/commit_tooltip.rs                                       |  13 
crates/git_ui/src/commit_view.rs                                          |   9 
crates/git_ui/src/conflict_view.rs                                        |   9 
crates/git_ui/src/file_history_view.rs                                    |   9 
crates/git_ui/src/git_ui.rs                                               |   3 
crates/git_ui/src/project_diff.rs                                         |  16 
crates/keymap_editor/src/keymap_editor.rs                                 |   8 
crates/language_models/src/provider/bedrock.rs                            |   3 
crates/language_models/src/provider/cloud.rs                              |   6 
crates/language_models/src/provider/lmstudio.rs                           |  34 
crates/language_models/src/provider/ollama.rs                             |  38 
crates/language_models/src/provider/open_ai.rs                            |   8 
crates/language_models/src/provider/open_ai_compatible.rs                 |   4 
crates/language_onboarding/src/python.rs                                  |   4 
crates/language_tools/src/lsp_log_view.rs                                 |  35 
crates/onboarding/src/basics_page.rs                                      |   8 
crates/onboarding/src/multibuffer_hint.rs                                 |   9 
crates/panel/src/panel.rs                                                 |   1 
crates/recent_projects/src/disconnected_overlay.rs                        |   9 
crates/recent_projects/src/remote_servers.rs                              |   6 
crates/repl/src/components/kernel_options.rs                              |   9 
crates/repl/src/notebook/notebook_ui.rs                                   |   9 
crates/rules_library/src/rules_library.rs                                 |   9 
crates/search/src/project_search.rs                                       |  20 
crates/settings_ui/src/pages/tool_permissions_setup.rs                    |  17 
crates/settings_ui/src/settings_ui.rs                                     |  31 
crates/theme_selector/src/icon_theme_selector.rs                          |   9 
crates/theme_selector/src/theme_selector.rs                               |   9 
crates/title_bar/src/title_bar.rs                                         |  25 
crates/ui/src/components/ai/configured_api_card.rs                        |   9 
crates/ui/src/components/banner.rs                                        |  10 
crates/ui/src/components/button.rs                                        |   1 
crates/ui/src/components/button/button.rs                                 | 226 
crates/ui/src/components/button/button_icon.rs                            | 199 
crates/ui/src/components/button/icon_button.rs                            |  49 
crates/ui/src/components/dropdown_menu.rs                                 |   9 
crates/workspace/src/notifications.rs                                     |  28 
64 files changed, 584 insertions(+), 847 deletions(-)

Detailed changes

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -332,10 +332,11 @@ impl AgentConfiguration {
                             .full_width()
                             .style(ButtonStyle::Outlined)
                             .layer(ElevationIndex::ModalSurface)
-                            .icon_position(IconPosition::Start)
-                            .icon(IconName::Thread)
-                            .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
+                            .start_icon(
+                                Icon::new(IconName::Thread)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .label_size(LabelSize::Small)
                             .on_click(cx.listener({
                                 let provider = provider.clone();
@@ -357,10 +358,11 @@ impl AgentConfiguration {
                                 )
                                 .full_width()
                                 .style(ButtonStyle::Outlined)
-                                .icon_position(IconPosition::Start)
-                                .icon(IconName::Trash)
-                                .icon_size(IconSize::Small)
-                                .icon_color(Color::Muted)
+                                .start_icon(
+                                    Icon::new(IconName::Trash)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .label_size(LabelSize::Small)
                                 .on_click(cx.listener({
                                     let provider = provider.clone();
@@ -426,10 +428,11 @@ impl AgentConfiguration {
             .trigger(
                 Button::new("add-provider", "Add Provider")
                     .style(ButtonStyle::Outlined)
-                    .icon_position(IconPosition::Start)
-                    .icon(IconName::Plus)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                    .start_icon(
+                        Icon::new(IconName::Plus)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .label_size(LabelSize::Small),
             )
             .menu({
@@ -525,10 +528,11 @@ impl AgentConfiguration {
             .trigger(
                 Button::new("add-server", "Add Server")
                     .style(ButtonStyle::Outlined)
-                    .icon_position(IconPosition::Start)
-                    .icon(IconName::Plus)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                    .start_icon(
+                        Icon::new(IconName::Plus)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .label_size(LabelSize::Small),
             )
             .menu({
@@ -970,10 +974,11 @@ impl AgentConfiguration {
             .trigger(
                 Button::new("add-agent", "Add Agent")
                     .style(ButtonStyle::Outlined)
-                    .icon_position(IconPosition::Start)
-                    .icon(IconName::Plus)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                    .start_icon(
+                        Icon::new(IconName::Plus)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .label_size(LabelSize::Small),
             )
             .menu({

crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs 🔗

@@ -340,10 +340,11 @@ impl AddLlmProviderModal {
                     .child(Label::new("Models").size(LabelSize::Small))
                     .child(
                         Button::new("add-model", "Add Model")
-                            .icon(IconName::Plus)
-                            .icon_position(IconPosition::Start)
-                            .icon_size(IconSize::XSmall)
-                            .icon_color(Color::Muted)
+                            .start_icon(
+                                Icon::new(IconName::Plus)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Muted),
+                            )
                             .label_size(LabelSize::Small)
                             .on_click(cx.listener(|this, _, window, cx| {
                                 this.input.add_model(window, cx);
@@ -446,10 +447,11 @@ impl AddLlmProviderModal {
             .when(has_more_than_one_model, |this| {
                 this.child(
                     Button::new(("remove-model", ix), "Remove Model")
-                        .icon(IconName::Trash)
-                        .icon_position(IconPosition::Start)
-                        .icon_size(IconSize::XSmall)
-                        .icon_color(Color::Muted)
+                        .start_icon(
+                            Icon::new(IconName::Trash)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
                         .label_size(LabelSize::Small)
                         .style(ButtonStyle::Outlined)
                         .full_width()

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs 🔗

@@ -693,9 +693,11 @@ impl ConfigureContextServerModal {
                 {
                     Some(
                         Button::new("open-repository", "Open Repository")
-                            .icon(IconName::ArrowUpRight)
-                            .icon_color(Color::Muted)
-                            .icon_size(IconSize::Small)
+                            .end_icon(
+                                Icon::new(IconName::ArrowUpRight)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .tooltip({
                                 let repository_url = repository_url.clone();
                                 move |_window, cx| {

crates/agent_ui/src/agent_diff.rs 🔗

@@ -686,10 +686,11 @@ impl Render for AgentDiffPane {
                         .child(
                             Button::new("continue-iterating", "Continue Iterating")
                                 .style(ButtonStyle::Filled)
-                                .icon(IconName::ForwardArrow)
-                                .icon_position(IconPosition::Start)
-                                .icon_size(IconSize::Small)
-                                .icon_color(Color::Muted)
+                                .start_icon(
+                                    Icon::new(IconName::ForwardArrow)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .full_width()
                                 .key_binding(KeyBinding::for_action_in(
                                     &ToggleFocus,

crates/agent_ui/src/agent_model_selector.rs 🔗

@@ -9,7 +9,7 @@ use language_model::IconOrSvg;
 use picker::popover_menu::PickerPopoverMenu;
 use settings::update_settings_file;
 use std::sync::Arc;
-use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
+use ui::{PopoverMenuHandle, Tooltip, prelude::*};
 
 pub struct AgentModelSelector {
     selector: Entity<LanguageModelSelector>,
@@ -112,9 +112,11 @@ impl Render for AgentModelSelector {
 
         PickerPopoverMenu::new(
             self.selector.clone(),
-            ButtonLike::new("active-model")
+            Button::new("active-model", model_name)
+                .label_size(LabelSize::Small)
+                .color(color)
                 .when_some(provider_icon, |this, icon| {
-                    this.child(
+                    this.start_icon(
                         match icon {
                             IconOrSvg::Svg(path) => Icon::from_external_svg(path),
                             IconOrSvg::Icon(name) => Icon::new(name),
@@ -123,14 +125,7 @@ impl Render for AgentModelSelector {
                         .size(IconSize::XSmall),
                     )
                 })
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .child(
-                    Label::new(model_name)
-                        .color(color)
-                        .size(LabelSize::Small)
-                        .ml_0p5(),
-                )
-                .child(
+                .end_icon(
                     Icon::new(IconName::ChevronDown)
                         .color(color)
                         .size(IconSize::XSmall),

crates/agent_ui/src/agent_panel.rs 🔗

@@ -80,9 +80,8 @@ use search::{BufferSearchBar, buffer_search};
 use settings::{Settings, update_settings_file};
 use theme::ThemeSettings;
 use ui::{
-    Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, Indicator,
-    KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
-    utils::WithRemSize,
+    Button, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, Indicator, KeyBinding,
+    PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, Tooltip, prelude::*, utils::WithRemSize,
 };
 use util::{ResultExt as _, debug_panic};
 use workspace::{
@@ -3632,11 +3631,7 @@ impl AgentPanel {
         };
 
         let trigger_button = Button::new("thread-target-trigger", trigger_label)
-            .icon(icon)
-            .icon_size(IconSize::XSmall)
-            .icon_position(IconPosition::End)
-            .icon_color(Color::Muted)
-            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
             .disabled(is_creating);
 
         let dock_position = AgentSettings::get_global(cx).dock;
@@ -4290,32 +4285,22 @@ impl AgentPanel {
                     (IconName::ChevronDown, Color::Muted, Color::Default)
                 };
 
-            let agent_icon_element: AnyElement =
-                if let Some(icon_path) = selected_agent_custom_icon_for_button {
-                    Icon::from_external_svg(icon_path)
-                        .size(IconSize::Small)
-                        .color(icon_color)
-                        .into_any_element()
-                } else {
-                    let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
-                    Icon::new(icon_name)
-                        .size(IconSize::Small)
-                        .color(icon_color)
-                        .into_any_element()
-                };
+            let agent_icon = if let Some(icon_path) = selected_agent_custom_icon_for_button {
+                Icon::from_external_svg(icon_path)
+                    .size(IconSize::Small)
+                    .color(icon_color)
+            } else {
+                let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
+                Icon::new(icon_name).size(IconSize::Small).color(icon_color)
+            };
 
-            let agent_selector_button = ButtonLike::new("agent-selector-trigger")
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .child(
-                    h_flex()
-                        .gap_1()
-                        .child(agent_icon_element)
-                        .child(Label::new(selected_agent_label).color(label_color).ml_0p5())
-                        .child(
-                            Icon::new(chevron_icon)
-                                .color(icon_color)
-                                .size(IconSize::XSmall),
-                        ),
+            let agent_selector_button = Button::new("agent-selector-trigger", selected_agent_label)
+                .start_icon(agent_icon)
+                .color(label_color)
+                .end_icon(
+                    Icon::new(chevron_icon)
+                        .color(icon_color)
+                        .size(IconSize::XSmall),
                 );
 
             let agent_selector_menu = PopoverMenu::new("new_thread_menu")

crates/agent_ui/src/agent_registry_ui.rs 🔗

@@ -467,10 +467,11 @@ impl AgentRegistryPage {
                 let agent_id = agent.id().to_string();
                 Button::new(button_id, "Install")
                     .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                    .icon(IconName::Download)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
-                    .icon_position(IconPosition::Start)
+                    .start_icon(
+                        Icon::new(IconName::Download)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .on_click(move |_, _, cx| {
                         let agent_id = agent_id.clone();
                         update_settings_file(fs.clone(), cx, move |settings, _| {
@@ -541,9 +542,11 @@ impl Render for AgentRegistryPage {
                                 Button::new("learn-more", "Learn More")
                                     .style(ButtonStyle::Outlined)
                                     .size(ButtonSize::Medium)
-                                    .icon(IconName::ArrowUpRight)
-                                    .icon_color(Color::Muted)
-                                    .icon_size(IconSize::Small)
+                                    .end_icon(
+                                        Icon::new(IconName::ArrowUpRight)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted),
+                                    )
                                     .on_click(move |_, _, cx| {
                                         cx.open_url(&zed_urls::acp_registry_blog(cx))
                                     }),

crates/agent_ui/src/config_options.rs 🔗

@@ -350,10 +350,7 @@ impl ConfigOptionSelector {
         )
         .label_size(LabelSize::Small)
         .color(Color::Muted)
-        .icon(icon)
-        .icon_size(IconSize::XSmall)
-        .icon_position(IconPosition::End)
-        .icon_color(Color::Muted)
+        .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
         .disabled(self.setting_value)
     }
 }

crates/agent_ui/src/connection_view/thread_view.rs 🔗

@@ -3826,11 +3826,8 @@ impl ThreadView {
                                 .child(Divider::horizontal())
                                 .child(
                                     Button::new("restore-checkpoint", "Restore Checkpoint")
-                                        .icon(IconName::Undo)
-                                        .icon_size(IconSize::XSmall)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(Icon::new(IconName::Undo).size(IconSize::XSmall).color(Color::Muted))
                                         .label_size(LabelSize::XSmall)
-                                        .icon_color(Color::Muted)
                                         .color(Color::Muted)
                                         .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
                                         .on_click(cx.listener(move |this, _, _window, cx| {
@@ -5783,10 +5780,11 @@ impl ThreadView {
                     .gap_0p5()
                     .child(
                         Button::new(("allow-btn", entry_ix), "Allow")
-                            .icon(IconName::Check)
-                            .icon_color(Color::Success)
-                            .icon_position(IconPosition::Start)
-                            .icon_size(IconSize::XSmall)
+                            .start_icon(
+                                Icon::new(IconName::Check)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Success),
+                            )
                             .label_size(LabelSize::Small)
                             .when(is_first, |this| {
                                 this.key_binding(
@@ -5817,10 +5815,11 @@ impl ThreadView {
                     )
                     .child(
                         Button::new(("deny-btn", entry_ix), "Deny")
-                            .icon(IconName::Close)
-                            .icon_color(Color::Error)
-                            .icon_position(IconPosition::Start)
-                            .icon_size(IconSize::XSmall)
+                            .start_icon(
+                                Icon::new(IconName::Close)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Error),
+                            )
                             .label_size(LabelSize::Small)
                             .when(is_first, |this| {
                                 this.key_binding(
@@ -5887,9 +5886,11 @@ impl ThreadView {
             .with_handle(permission_dropdown_handle)
             .trigger(
                 Button::new(("granularity-trigger", entry_ix), current_label)
-                    .icon(IconName::ChevronDown)
-                    .icon_size(IconSize::XSmall)
-                    .icon_color(Color::Muted)
+                    .end_icon(
+                        Icon::new(IconName::ChevronDown)
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
                     .label_size(LabelSize::Small)
                     .when(is_first, |this| {
                         this.key_binding(
@@ -5962,24 +5963,35 @@ impl ThreadView {
                 let option_id = SharedString::from(option.option_id.0.clone());
                 Button::new((option_id, entry_ix), option.name.clone())
                     .map(|this| {
-                        let (this, action) = match option.kind {
+                        let (icon, action) = match option.kind {
                             acp::PermissionOptionKind::AllowOnce => (
-                                this.icon(IconName::Check).icon_color(Color::Success),
+                                Icon::new(IconName::Check)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Success),
                                 Some(&AllowOnce as &dyn Action),
                             ),
                             acp::PermissionOptionKind::AllowAlways => (
-                                this.icon(IconName::CheckDouble).icon_color(Color::Success),
+                                Icon::new(IconName::CheckDouble)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Success),
                                 Some(&AllowAlways as &dyn Action),
                             ),
                             acp::PermissionOptionKind::RejectOnce => (
-                                this.icon(IconName::Close).icon_color(Color::Error),
+                                Icon::new(IconName::Close)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Error),
                                 Some(&RejectOnce as &dyn Action),
                             ),
-                            acp::PermissionOptionKind::RejectAlways | _ => {
-                                (this.icon(IconName::Close).icon_color(Color::Error), None)
-                            }
+                            acp::PermissionOptionKind::RejectAlways | _ => (
+                                Icon::new(IconName::Close)
+                                    .size(IconSize::XSmall)
+                                    .color(Color::Error),
+                                None,
+                            ),
                         };
 
+                        let this = this.start_icon(icon);
+
                         let Some(action) = action else {
                             return this;
                         };
@@ -5995,8 +6007,6 @@ impl ThreadView {
                                 .map(|kb| kb.size(rems_from_px(10.))),
                         )
                     })
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::XSmall)
                     .label_size(LabelSize::Small)
                     .on_click(cx.listener({
                         let session_id = session_id.clone();
@@ -6373,9 +6383,11 @@ impl ThreadView {
                     .color(Color::Muted)
                     .truncate(true)
                     .when(is_file.is_none(), |this| {
-                        this.icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
-                            .icon_color(Color::Muted)
+                        this.end_icon(
+                            Icon::new(IconName::ArrowUpRight)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
                     })
                     .on_click(cx.listener({
                         let workspace = self.workspace.clone();
@@ -7470,19 +7482,16 @@ impl ThreadView {
             .title("Codex on Windows")
             .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
             .actions_slot(
-                Button::new("open-wsl-modal", "Open in WSL")
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
-                    .on_click(cx.listener({
-                        move |_, _, _window, cx| {
-                            #[cfg(windows)]
-                            _window.dispatch_action(
-                                zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
-                                cx,
-                            );
-                            cx.notify();
-                        }
-                    })),
+                Button::new("open-wsl-modal", "Open in WSL").on_click(cx.listener({
+                    move |_, _, _window, cx| {
+                        #[cfg(windows)]
+                        _window.dispatch_action(
+                            zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
+                            cx,
+                        );
+                        cx.notify();
+                    }
+                })),
             )
             .dismiss_action(
                 IconButton::new("dismiss", IconName::Close)

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -796,9 +796,11 @@ impl<T: 'static> PromptEditor<T> {
                 vec![
                     Button::new("start", mode.start_label())
                         .label_size(LabelSize::Small)
-                        .icon(IconName::Return)
-                        .icon_size(IconSize::XSmall)
-                        .icon_color(Color::Muted)
+                        .end_icon(
+                            Icon::new(IconName::Return)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
                         .on_click(
                             cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
                         )

crates/agent_ui/src/message_editor.rs 🔗

@@ -33,7 +33,7 @@ use rope::Point;
 use settings::Settings;
 use std::{cell::RefCell, fmt::Write, ops::Range, rc::Rc, sync::Arc};
 use theme::ThemeSettings;
-use ui::{ButtonLike, ButtonStyle, ContextMenu, Disclosure, ElevationIndex, prelude::*};
+use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};
 use util::paths::PathStyle;
 use util::{ResultExt, debug_panic};
 use workspace::{CollaboratorId, Workspace};
@@ -1161,11 +1161,9 @@ impl MessageEditor {
                 render: Arc::new({
                     let title = title.clone();
                     move |_fold_id, _fold_range, _cx| {
-                        ButtonLike::new("crease")
-                            .style(ButtonStyle::Filled)
+                        Button::new("crease", title.clone())
                             .layer(ElevationIndex::ElevatedSurface)
-                            .child(Icon::new(icon))
-                            .child(Label::new(title.clone()).single_line())
+                            .start_icon(Icon::new(icon))
                             .into_any_element()
                     }
                 }),

crates/agent_ui/src/mode_selector.rs 🔗

@@ -169,10 +169,7 @@ impl Render for ModeSelector {
         let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
             .label_size(LabelSize::Small)
             .color(Color::Muted)
-            .icon(icon)
-            .icon_size(IconSize::XSmall)
-            .icon_position(IconPosition::End)
-            .icon_color(Color::Muted)
+            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
             .disabled(self.setting_mode);
 
         PopoverMenu::new("mode-selector")

crates/agent_ui/src/model_selector_popover.rs 🔗

@@ -5,7 +5,7 @@ use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
 use fs::Fs;
 use gpui::{AnyView, Entity, FocusHandle};
 use picker::popover_menu::PickerPopoverMenu;
-use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
+use ui::{PopoverMenuHandle, Tooltip, prelude::*};
 
 use crate::ui::ModelSelectorTooltip;
 use crate::{ModelSelector, model_selector::acp_model_selector};
@@ -96,11 +96,12 @@ impl Render for ModelSelectorPopover {
 
         PickerPopoverMenu::new(
             self.selector.clone(),
-            ButtonLike::new("active-model")
+            Button::new("active-model", model_name)
+                .label_size(LabelSize::Small)
+                .color(color)
                 .disabled(self.disabled)
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                 .when_some(model_icon, |this, icon| {
-                    this.child(
+                    this.start_icon(
                         match icon {
                             AgentModelIcon::Path(path) => Icon::from_external_svg(path),
                             AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
@@ -109,13 +110,7 @@ impl Render for ModelSelectorPopover {
                         .size(IconSize::XSmall),
                     )
                 })
-                .child(
-                    Label::new(model_name)
-                        .color(color)
-                        .size(LabelSize::Small)
-                        .ml_0p5(),
-                )
-                .child(
+                .end_icon(
                     Icon::new(icon)
                         .map(|this| {
                             if self.disabled {

crates/agent_ui/src/profile_selector.rs 🔗

@@ -16,7 +16,7 @@ use std::{
 };
 use ui::{
     DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
-    ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
+    ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
 };
 
 /// Trait for types that can provide and manage agent profiles
@@ -192,11 +192,7 @@ impl Render for ProfileSelector {
             .disabled(self.disabled)
             .label_size(LabelSize::Small)
             .color(Color::Muted)
-            .icon(icon)
-            .icon_size(IconSize::XSmall)
-            .icon_position(IconPosition::End)
-            .icon_color(Color::Muted)
-            .selected_style(ButtonStyle::Tinted(TintColor::Accent));
+            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
 
         let disabled = self.disabled;
 

crates/agent_ui/src/sidebar.rs 🔗

@@ -1775,10 +1775,11 @@ impl Sidebar {
                 )
                 .full_width()
                 .style(ButtonStyle::Outlined)
-                .icon(IconName::Plus)
-                .icon_color(Color::Muted)
-                .icon_size(IconSize::Small)
-                .icon_position(IconPosition::Start)
+                .start_icon(
+                    Icon::new(IconName::Plus)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
                 .toggle_state(is_selected)
                 .on_click(cx.listener(move |this, _, window, cx| {
                     this.selection = None;
@@ -1833,10 +1834,11 @@ impl Sidebar {
                     .full_width()
                     .label_size(LabelSize::Small)
                     .style(ButtonStyle::Outlined)
-                    .icon(IconName::Archive)
-                    .icon_color(Color::Muted)
-                    .icon_size(IconSize::XSmall)
-                    .icon_position(IconPosition::Start)
+                    .start_icon(
+                        Icon::new(IconName::Archive)
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
                     .on_click(cx.listener(|this, _, window, cx| {
                         this.show_archive(window, cx);
                     })),

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -1191,11 +1191,11 @@ impl TextThreadEditor {
                                     Button::new("show-error", "Error")
                                         .color(Color::Error)
                                         .selected_label_color(Color::Error)
-                                        .selected_icon_color(Color::Error)
-                                        .icon(IconName::XCircle)
-                                        .icon_color(Color::Error)
-                                        .icon_size(IconSize::XSmall)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(
+                                            Icon::new(IconName::XCircle)
+                                                .size(IconSize::XSmall)
+                                                .color(Color::Error),
+                                        )
                                         .tooltip(Tooltip::text("View Details"))
                                         .on_click({
                                             let text_thread = text_thread.clone();
@@ -2287,20 +2287,11 @@ impl TextThreadEditor {
 
         PickerPopoverMenu::new(
             self.language_model_selector.clone(),
-            ButtonLike::new("active-model")
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .child(
-                    h_flex()
-                        .gap_0p5()
-                        .child(provider_icon_element)
-                        .child(
-                            Label::new(model_name)
-                                .color(color)
-                                .size(LabelSize::Small)
-                                .ml_0p5(),
-                        )
-                        .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
-                ),
+            Button::new("active-model", model_name)
+                .color(color)
+                .label_size(LabelSize::Small)
+                .start_icon(provider_icon_element)
+                .end_icon(Icon::new(icon).color(color).size(IconSize::XSmall)),
             tooltip,
             gpui::Corner::BottomRight,
             cx,

crates/agent_ui/src/ui/acp_onboarding_modal.rs 🔗

@@ -193,15 +193,16 @@ impl Render for AcpOnboardingModal {
         let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
 
         let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
-            .icon_size(IconSize::Indicator)
             .style(ButtonStyle::Tinted(TintColor::Accent))
             .full_width()
             .on_click(cx.listener(Self::open_panel));
 
         let docs_button = Button::new("add-other-agents", "Add Other Agents")
-            .icon(IconName::ArrowUpRight)
-            .icon_size(IconSize::Indicator)
-            .icon_color(Color::Muted)
+            .end_icon(
+                Icon::new(IconName::ArrowUpRight)
+                    .size(IconSize::Indicator)
+                    .color(Color::Muted),
+            )
             .full_width()
             .on_click(cx.listener(Self::open_agent_registry));
 

crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs 🔗

@@ -201,15 +201,16 @@ impl Render for ClaudeCodeOnboardingModal {
         let copy = "Powered by the Agent Client Protocol, you can now run Claude Agent as\na first-class citizen in Zed's agent panel.";
 
         let open_panel_button = Button::new("open-panel", "Start with Claude Agent")
-            .icon_size(IconSize::Indicator)
             .style(ButtonStyle::Tinted(TintColor::Accent))
             .full_width()
             .on_click(cx.listener(Self::open_panel));
 
         let docs_button = Button::new("add-other-agents", "Add Other Agents")
-            .icon(IconName::ArrowUpRight)
-            .icon_size(IconSize::Indicator)
-            .icon_color(Color::Muted)
+            .end_icon(
+                Icon::new(IconName::ArrowUpRight)
+                    .size(IconSize::Indicator)
+                    .color(Color::Muted),
+            )
             .full_width()
             .on_click(cx.listener(Self::view_docs));
 

crates/collab_ui/src/collab_panel.rs 🔗

@@ -2347,9 +2347,7 @@ impl CollabPanel {
                     .gap_2()
                     .child(
                         Button::new("sign_in", button_label)
-                            .icon_color(Color::Muted)
-                            .icon(IconName::Github)
-                            .icon_position(IconPosition::Start)
+                            .start_icon(Icon::new(IconName::Github).color(Color::Muted))
                             .style(ButtonStyle::Filled)
                             .full_width()
                             .disabled(is_signing_in)
@@ -2597,9 +2595,9 @@ impl CollabPanel {
             Section::Channels => {
                 Some(
                     h_flex()
-                        .gap_1()
                         .child(
                             IconButton::new("filter-active-channels", IconName::ListFilter)
+                                .icon_size(IconSize::Small)
                                 .toggle_state(self.filter_active_channels)
                                 .when(!self.filter_active_channels, |button| {
                                     button.visible_on_hover("section-header")

crates/collab_ui/src/notification_panel.rs 🔗

@@ -544,9 +544,7 @@ impl Render for NotificationPanel {
                             .p_4()
                             .child(
                                 Button::new("connect_prompt_button", "Connect")
-                                    .icon_color(Color::Muted)
-                                    .icon(IconName::Github)
-                                    .icon_position(IconPosition::Start)
+                                    .start_icon(Icon::new(IconName::Github).color(Color::Muted))
                                     .style(ButtonStyle::Filled)
                                     .full_width()
                                     .on_click({

crates/copilot_ui/src/sign_in.rs 🔗

@@ -387,10 +387,11 @@ impl CopilotCodeVerification {
                     .full_width()
                     .style(ButtonStyle::Outlined)
                     .size(ButtonSize::Medium)
-                    .icon(IconName::Download)
-                    .icon_color(Color::Muted)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(
+                        Icon::new(IconName::Download)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .on_click(move |_, window, cx| {
                         reinstall_and_sign_in(copilot.clone(), window, cx)
                     }),
@@ -570,10 +571,11 @@ impl ConfigurationView {
                 }
             })
             .style(ButtonStyle::Outlined)
-            .icon(IconName::Github)
-            .icon_color(Color::Muted)
-            .icon_position(IconPosition::Start)
-            .icon_size(IconSize::Small)
+            .start_icon(
+                Icon::new(IconName::Github)
+                    .size(IconSize::Small)
+                    .color(Color::Muted),
+            )
             .when(edit_prediction, |this| this.tab_index(0isize))
             .on_click(|_, window, cx| {
                 if let Some(app_state) = AppState::global(cx).upgrade()
@@ -600,10 +602,11 @@ impl ConfigurationView {
                 }
             })
             .style(ButtonStyle::Outlined)
-            .icon(IconName::Download)
-            .icon_color(Color::Muted)
-            .icon_position(IconPosition::Start)
-            .icon_size(IconSize::Small)
+            .start_icon(
+                Icon::new(IconName::Download)
+                    .size(IconSize::Small)
+                    .color(Color::Muted),
+            )
             .on_click(|_, window, cx| {
                 if let Some(app_state) = AppState::global(cx).upgrade()
                     && let Some(copilot) = GlobalCopilotAuth::try_get_or_init(app_state, cx)

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1821,20 +1821,22 @@ impl Render for DebugPanel {
                         .gap_2()
                         .child(
                             Button::new("spawn-new-session-empty-state", "New Session")
-                                .icon(IconName::Plus)
-                                .icon_size(IconSize::Small)
-                                .icon_color(Color::Muted)
-                                .icon_position(IconPosition::Start)
+                                .start_icon(
+                                    Icon::new(IconName::Plus)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(crate::Start.boxed_clone(), cx);
                                 }),
                         )
                         .child(
                             Button::new("edit-debug-settings", "Edit debug.json")
-                                .icon(IconName::Code)
-                                .icon_size(IconSize::Small)
-                                .icon_color(Color::Muted)
-                                .icon_position(IconPosition::Start)
+                                .start_icon(
+                                    Icon::new(IconName::Code)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(
                                         zed_actions::OpenProjectDebugTasks.boxed_clone(),
@@ -1844,10 +1846,11 @@ impl Render for DebugPanel {
                         )
                         .child(
                             Button::new("open-debugger-docs", "Debugger Docs")
-                                .icon(IconName::Book)
-                                .icon_size(IconSize::Small)
-                                .icon_color(Color::Muted)
-                                .icon_position(IconPosition::Start)
+                                .start_icon(
+                                    Icon::new(IconName::Book)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
                         )
                         .child(
@@ -1855,10 +1858,11 @@ impl Render for DebugPanel {
                                 "spawn-new-session-install-extensions",
                                 "Debugger Extensions",
                             )
-                            .icon(IconName::Blocks)
-                            .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .icon_position(IconPosition::Start)
+                            .start_icon(
+                                Icon::new(IconName::Blocks)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .on_click(|_, window, cx| {
                                 window.dispatch_action(
                                     zed_actions::Extensions {

crates/edit_prediction_ui/src/rate_prediction_modal.rs 🔗

@@ -765,9 +765,7 @@ impl RatePredictionsModal {
                                 .gap_1()
                                 .child(
                                     Button::new("bad", "Bad Prediction")
-                                        .icon(IconName::ThumbsDown)
-                                        .icon_size(IconSize::Small)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(Icon::new(IconName::ThumbsDown).size(IconSize::Small))
                                         .disabled(rated || feedback_empty)
                                         .when(feedback_empty, |this| {
                                             this.tooltip(Tooltip::text(
@@ -791,9 +789,7 @@ impl RatePredictionsModal {
                                 )
                                 .child(
                                     Button::new("good", "Good Prediction")
-                                        .icon(IconName::ThumbsUp)
-                                        .icon_size(IconSize::Small)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(Icon::new(IconName::ThumbsUp).size(IconSize::Small))
                                         .disabled(rated)
                                         .key_binding(KeyBinding::for_action_in(
                                             &ThumbsUpActivePrediction,

crates/extensions_ui/src/extensions_ui.rs 🔗

@@ -1056,10 +1056,11 @@ impl ExtensionsPage {
                     "Install",
                 )
                 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                .icon(IconName::Download)
-                .icon_size(IconSize::Small)
-                .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start)
+                .start_icon(
+                    Icon::new(IconName::Download)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
                 .on_click({
                     let extension_id = extension.id.clone();
                     move |_, _, cx| {
@@ -1078,10 +1079,11 @@ impl ExtensionsPage {
                     "Install",
                 )
                 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                .icon(IconName::Download)
-                .icon_size(IconSize::Small)
-                .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start)
+                .start_icon(
+                    Icon::new(IconName::Download)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
                 .disabled(true),
                 configure: None,
                 upgrade: None,
@@ -1479,10 +1481,11 @@ impl ExtensionsPage {
                 }
             });
         let open_registry_button = Button::new("open_registry", "Learn More")
-            .icon(IconName::ArrowUpRight)
-            .icon_size(IconSize::Small)
-            .icon_position(IconPosition::End)
-            .icon_color(Color::Muted)
+            .end_icon(
+                Icon::new(IconName::ArrowUpRight)
+                    .size(IconSize::Small)
+                    .color(Color::Muted),
+            )
             .on_click({
                 move |_event, _window, cx| {
                     telemetry::event!(
@@ -1520,9 +1523,7 @@ impl ExtensionsPage {
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let docs_url_button = Button::new("open_docs", "View Documentation")
-            .icon(IconName::ArrowUpRight)
-            .icon_size(IconSize::Small)
-            .icon_position(IconPosition::End)
+            .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::Small))
             .on_click({
                 move |_event, _window, cx| {
                     telemetry::event!(

crates/git_graph/src/git_graph.rs 🔗

@@ -1494,10 +1494,9 @@ impl GitGraph {
 
                                 this.child(
                                     Button::new("author-email-copy", author_email.clone())
-                                        .icon(icon)
-                                        .icon_size(IconSize::Small)
-                                        .icon_color(icon_color)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(
+                                            Icon::new(icon).size(IconSize::Small).color(icon_color),
+                                        )
                                         .label_size(LabelSize::Small)
                                         .truncate(true)
                                         .color(Color::Muted)
@@ -1542,10 +1541,9 @@ impl GitGraph {
                                 };
 
                                 Button::new("sha-button", &full_sha)
-                                    .icon(icon)
-                                    .icon_size(IconSize::Small)
-                                    .icon_color(icon_color)
-                                    .icon_position(IconPosition::Start)
+                                    .start_icon(
+                                        Icon::new(icon).size(IconSize::Small).color(icon_color),
+                                    )
                                     .label_size(LabelSize::Small)
                                     .truncate(true)
                                     .color(Color::Muted)
@@ -1602,10 +1600,9 @@ impl GitGraph {
                                         "view-on-provider",
                                         format!("View on {}", provider_name),
                                     )
-                                    .icon(icon)
-                                    .icon_size(IconSize::Small)
-                                    .icon_color(Color::Muted)
-                                    .icon_position(IconPosition::Start)
+                                    .start_icon(
+                                        Icon::new(icon).size(IconSize::Small).color(Color::Muted),
+                                    )
                                     .label_size(LabelSize::Small)
                                     .truncate(true)
                                     .color(Color::Muted)

crates/git_ui/src/blame_ui.rs 🔗

@@ -322,10 +322,11 @@ impl BlameRenderer for GitBlameRenderer {
                                                         format!("#{}", pr.number),
                                                     )
                                                     .color(Color::Muted)
-                                                    .icon(IconName::PullRequest)
-                                                    .icon_color(Color::Muted)
-                                                    .icon_position(IconPosition::Start)
-                                                    .icon_size(IconSize::Small)
+                                                    .start_icon(
+                                                        Icon::new(IconName::PullRequest)
+                                                            .size(IconSize::Small)
+                                                            .color(Color::Muted),
+                                                    )
                                                     .on_click(move |_, _, cx| {
                                                         cx.stop_propagation();
                                                         cx.open_url(pr.url.as_str())
@@ -339,10 +340,11 @@ impl BlameRenderer for GitBlameRenderer {
                                                     short_commit_id.clone(),
                                                 )
                                                 .color(Color::Muted)
-                                                .icon(IconName::FileGit)
-                                                .icon_color(Color::Muted)
-                                                .icon_position(IconPosition::Start)
-                                                .icon_size(IconSize::Small)
+                                                .start_icon(
+                                                    Icon::new(IconName::FileGit)
+                                                        .size(IconSize::Small)
+                                                        .color(Color::Muted),
+                                                )
                                                 .on_click(move |_, window, cx| {
                                                     CommitView::open(
                                                         commit_summary.sha.clone().into(),

crates/git_ui/src/commit_modal.rs 🔗

@@ -366,11 +366,12 @@ impl CommitModal {
             .unwrap_or_else(|| "<no branch>".to_owned());
 
         let branch_picker_button = panel_button(branch)
-            .icon(IconName::GitBranch)
-            .icon_size(IconSize::Small)
-            .icon_color(Color::Placeholder)
+            .start_icon(
+                Icon::new(IconName::GitBranch)
+                    .size(IconSize::Small)
+                    .color(Color::Placeholder),
+            )
             .color(Color::Muted)
-            .icon_position(IconPosition::Start)
             .on_click(cx.listener(|_, _, window, cx| {
                 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
             }))

crates/git_ui/src/commit_tooltip.rs 🔗

@@ -336,9 +336,10 @@ impl Render for CommitTooltip {
                                                     format!("#{}", pr.number),
                                                 )
                                                 .color(Color::Muted)
-                                                .icon(IconName::PullRequest)
-                                                .icon_color(Color::Muted)
-                                                .icon_position(IconPosition::Start)
+                                                .start_icon(
+                                                    Icon::new(IconName::PullRequest)
+                                                        .color(Color::Muted),
+                                                )
                                                 .style(ButtonStyle::Subtle)
                                                 .on_click(move |_, _, cx| {
                                                     cx.stop_propagation();
@@ -354,9 +355,9 @@ impl Render for CommitTooltip {
                                             )
                                             .style(ButtonStyle::Subtle)
                                             .color(Color::Muted)
-                                            .icon(IconName::FileGit)
-                                            .icon_color(Color::Muted)
-                                            .icon_position(IconPosition::Start)
+                                            .start_icon(
+                                                Icon::new(IconName::FileGit).color(Color::Muted),
+                                            )
                                             .on_click(
                                                 move |_, window, cx| {
                                                     CommitView::open(

crates/git_ui/src/commit_view.rs 🔗

@@ -524,10 +524,11 @@ impl CommitView {
             .when(self.stash.is_none(), |this| {
                 this.child(
                     Button::new("sha", "Commit SHA")
-                        .icon(copy_icon)
-                        .icon_color(copy_icon_color)
-                        .icon_position(IconPosition::Start)
-                        .icon_size(IconSize::Small)
+                        .start_icon(
+                            Icon::new(copy_icon)
+                                .size(IconSize::Small)
+                                .color(copy_icon_color),
+                        )
                         .tooltip({
                             let commit_sha = commit_sha.clone();
                             move |_, cx| {

crates/git_ui/src/conflict_view.rs 🔗

@@ -453,10 +453,11 @@ fn render_conflict_buttons(
             this.child(Divider::vertical()).child(
                 Button::new("resolve-with-agent", "Resolve with Agent")
                     .label_size(LabelSize::Small)
-                    .icon(IconName::ZedAssistant)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                    .start_icon(
+                        Icon::new(IconName::ZedAssistant)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .on_click({
                         let conflict = conflict.clone();
                         move |_, window, cx| {

crates/git_ui/src/file_history_view.rs 🔗

@@ -429,10 +429,11 @@ impl Render for FileHistoryView {
                                     Button::new("load-more", "Load More")
                                         .disabled(self.loading_more)
                                         .label_size(LabelSize::Small)
-                                        .icon(IconName::ArrowCircle)
-                                        .icon_size(IconSize::Small)
-                                        .icon_color(Color::Muted)
-                                        .icon_position(IconPosition::Start)
+                                        .start_icon(
+                                            Icon::new(IconName::ArrowCircle)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
                                         .on_click(cx.listener(|this, _, window, cx| {
                                             this.load_more(window, cx);
                                         })),

crates/git_ui/src/git_ui.rs 🔗

@@ -872,8 +872,7 @@ impl Render for GitCloneModal {
                     .child(
                         Button::new("learn-more", "Learn More")
                             .label_size(LabelSize::Small)
-                            .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::XSmall)
+                            .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall))
                             .on_click(|_, _, cx| {
                                 cx.open_url("https://github.com/git-guides/git-clone");
                             }),

crates/git_ui/src/project_diff.rs 🔗

@@ -1592,8 +1592,11 @@ fn render_send_review_to_agent_button(review_count: usize, focus_handle: &FocusH
         "send-review",
         format!("Send Review to Agent ({})", review_count),
     )
-    .icon(IconName::ZedAssistant)
-    .icon_position(IconPosition::Start)
+    .start_icon(
+        Icon::new(IconName::ZedAssistant)
+            .size(IconSize::Small)
+            .color(Color::Muted),
+    )
     .tooltip(Tooltip::for_action_title_in(
         "Send all review comments to the Agent panel",
         &SendReviewToAgent,
@@ -1686,10 +1689,11 @@ impl Render for BranchDiffToolbar {
                 let focus_handle = focus_handle.clone();
                 this.child(Divider::vertical()).child(
                     Button::new("review-diff", "Review Diff")
-                        .icon(IconName::ZedAssistant)
-                        .icon_position(IconPosition::Start)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted)
+                        .start_icon(
+                            Icon::new(IconName::ZedAssistant)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .key_binding(KeyBinding::for_action_in(&ReviewDiff, &focus_handle, cx))
                         .tooltip(move |_, cx| {
                             Tooltip::with_meta_in(

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -2928,9 +2928,11 @@ impl Render for KeybindingEditorModal {
                                                 .child(
                                                     Button::new("show_matching", "View")
                                                         .label_size(LabelSize::Small)
-                                                        .icon(IconName::ArrowUpRight)
-                                                        .icon_color(Color::Muted)
-                                                        .icon_size(IconSize::Small)
+                                                        .end_icon(
+                                                            Icon::new(IconName::ArrowUpRight)
+                                                                .size(IconSize::Small)
+                                                                .color(Color::Muted),
+                                                        )
                                                         .on_click(cx.listener(
                                                             |this, _, window, cx| {
                                                                 this.show_matching_bindings(

crates/language_models/src/provider/bedrock.rs 🔗

@@ -1574,7 +1574,8 @@ impl Render for ConfigurationView {
         }
 
         v_flex()
-            .size_full()
+            .min_w_0()
+            .w_full()
             .track_focus(&self.focus_handle)
             .on_action(cx.listener(Self::on_tab))
             .on_action(cx.listener(Self::on_tab_prev))

crates/language_models/src/provider/cloud.rs 🔗

@@ -1126,6 +1126,7 @@ impl RenderOnce for ZedAiConfiguration {
         let manage_subscription_buttons = if is_pro {
             Button::new("manage_settings", "Manage Subscription")
                 .full_width()
+                .label_size(LabelSize::Small)
                 .style(ButtonStyle::Tinted(TintColor::Accent))
                 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
                 .into_any_element()
@@ -1149,10 +1150,7 @@ impl RenderOnce for ZedAiConfiguration {
                 .child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models."))
                 .child(
                     Button::new("sign_in", "Sign In to use Zed AI")
-                        .icon_color(Color::Muted)
-                        .icon(IconName::Github)
-                        .icon_size(IconSize::Small)
-                        .icon_position(IconPosition::Start)
+                        .start_icon(Icon::new(IconName::Github).size(IconSize::Small).color(Color::Muted))
                         .full_width()
                         .on_click({
                             let callback = self.sign_in_callback.clone();

crates/language_models/src/provider/lmstudio.rs 🔗

@@ -820,9 +820,7 @@ impl ConfigurationView {
                 .child(
                     Button::new("reset-api-url", "Reset API URL")
                         .label_size(LabelSize::Small)
-                        .icon(IconName::Undo)
-                        .icon_size(IconSize::Small)
-                        .icon_position(IconPosition::Start)
+                        .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
                         .layer(ElevationIndex::ModalSurface)
                         .on_click(
                             cx.listener(|this, _, _window, cx| this.reset_api_url(_window, cx)),
@@ -918,9 +916,11 @@ impl Render for ConfigurationView {
                                     this.child(
                                         Button::new("lmstudio-site", "LM Studio")
                                             .style(ButtonStyle::Subtle)
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::Small)
-                                            .icon_color(Color::Muted)
+                                            .end_icon(
+                                                Icon::new(IconName::ArrowUpRight)
+                                                    .size(IconSize::Small)
+                                                    .color(Color::Muted),
+                                            )
                                             .on_click(move |_, _window, cx| {
                                                 cx.open_url(LMSTUDIO_SITE)
                                             })
@@ -933,9 +933,11 @@ impl Render for ConfigurationView {
                                             "Download LM Studio",
                                         )
                                         .style(ButtonStyle::Subtle)
-                                        .icon(IconName::ArrowUpRight)
-                                        .icon_size(IconSize::Small)
-                                        .icon_color(Color::Muted)
+                                        .end_icon(
+                                            Icon::new(IconName::ArrowUpRight)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
                                         .on_click(move |_, _window, cx| {
                                             cx.open_url(LMSTUDIO_DOWNLOAD_URL)
                                         })
@@ -946,9 +948,11 @@ impl Render for ConfigurationView {
                             .child(
                                 Button::new("view-models", "Model Catalog")
                                     .style(ButtonStyle::Subtle)
-                                    .icon(IconName::ArrowUpRight)
-                                    .icon_size(IconSize::Small)
-                                    .icon_color(Color::Muted)
+                                    .end_icon(
+                                        Icon::new(IconName::ArrowUpRight)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted),
+                                    )
                                     .on_click(move |_, _window, cx| {
                                         cx.open_url(LMSTUDIO_CATALOG_URL)
                                     }),
@@ -981,9 +985,9 @@ impl Render for ConfigurationView {
                         } else {
                             this.child(
                                 Button::new("retry_lmstudio_models", "Connect")
-                                    .icon_position(IconPosition::Start)
-                                    .icon_size(IconSize::XSmall)
-                                    .icon(IconName::PlayFilled)
+                                    .start_icon(
+                                        Icon::new(IconName::PlayFilled).size(IconSize::XSmall),
+                                    )
                                     .on_click(cx.listener(move |this, _, _window, cx| {
                                         this.retry_connection(_window, cx)
                                     })),

crates/language_models/src/provider/ollama.rs 🔗

@@ -858,9 +858,7 @@ impl ConfigurationView {
                 .child(
                     Button::new("reset-context-window", "Reset")
                         .label_size(LabelSize::Small)
-                        .icon(IconName::Undo)
-                        .icon_size(IconSize::Small)
-                        .icon_position(IconPosition::Start)
+                        .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
                         .layer(ElevationIndex::ModalSurface)
                         .on_click(
                             cx.listener(|this, _, window, cx| {
@@ -905,9 +903,7 @@ impl ConfigurationView {
                 .child(
                     Button::new("reset-api-url", "Reset API URL")
                         .label_size(LabelSize::Small)
-                        .icon(IconName::Undo)
-                        .icon_size(IconSize::Small)
-                        .icon_position(IconPosition::Start)
+                        .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
                         .layer(ElevationIndex::ModalSurface)
                         .on_click(
                             cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
@@ -949,9 +945,11 @@ impl Render for ConfigurationView {
                                     this.child(
                                         Button::new("ollama-site", "Ollama")
                                             .style(ButtonStyle::Subtle)
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
+                                            .end_icon(
+                                                Icon::new(IconName::ArrowUpRight)
+                                                    .size(IconSize::XSmall)
+                                                    .color(Color::Muted),
+                                            )
                                             .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE))
                                             .into_any_element(),
                                     )
@@ -959,9 +957,11 @@ impl Render for ConfigurationView {
                                     this.child(
                                         Button::new("download_ollama_button", "Download Ollama")
                                             .style(ButtonStyle::Subtle)
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
-                                            .icon_color(Color::Muted)
+                                            .end_icon(
+                                                Icon::new(IconName::ArrowUpRight)
+                                                    .size(IconSize::XSmall)
+                                                    .color(Color::Muted),
+                                            )
                                             .on_click(move |_, _, cx| {
                                                 cx.open_url(OLLAMA_DOWNLOAD_URL)
                                             })
@@ -972,9 +972,11 @@ impl Render for ConfigurationView {
                             .child(
                                 Button::new("view-models", "View All Models")
                                     .style(ButtonStyle::Subtle)
-                                    .icon(IconName::ArrowUpRight)
-                                    .icon_size(IconSize::XSmall)
-                                    .icon_color(Color::Muted)
+                                    .end_icon(
+                                        Icon::new(IconName::ArrowUpRight)
+                                            .size(IconSize::XSmall)
+                                            .color(Color::Muted),
+                                    )
                                     .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
                             ),
                     )
@@ -1005,9 +1007,9 @@ impl Render for ConfigurationView {
                         } else {
                             this.child(
                                 Button::new("retry_ollama_models", "Connect")
-                                    .icon_position(IconPosition::Start)
-                                    .icon_size(IconSize::XSmall)
-                                    .icon(IconName::PlayOutlined)
+                                    .start_icon(
+                                        Icon::new(IconName::PlayOutlined).size(IconSize::XSmall),
+                                    )
                                     .on_click(cx.listener(move |this, _, window, cx| {
                                         this.retry_connection(window, cx)
                                     })),

crates/language_models/src/provider/open_ai.rs 🔗

@@ -1415,9 +1415,11 @@ impl Render for ConfigurationView {
             )
             .child(
                 Button::new("docs", "Learn More")
-                    .icon(IconName::ArrowUpRight)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                    .end_icon(
+                        Icon::new(IconName::ArrowUpRight)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .on_click(move |_, _window, cx| {
                         cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible")
                     }),

crates/language_models/src/provider/open_ai_compatible.rs 🔗

@@ -545,9 +545,7 @@ impl Render for ConfigurationView {
                         .child(
                             Button::new("reset-api-key", "Reset API Key")
                                 .label_size(LabelSize::Small)
-                                .icon(IconName::Undo)
-                                .icon_size(IconSize::Small)
-                                .icon_position(IconPosition::Start)
+                                .start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
                                 .layer(ElevationIndex::ModalSurface)
                                 .when(env_var_set, |this| {
                                     this.tooltip(Tooltip::text(format!("To reset your API key, unset the {env_var_name} environment variable.")))

crates/language_onboarding/src/python.rs 🔗

@@ -56,10 +56,8 @@ impl Render for BasedPyrightBanner {
                                 .gap_0p5()
                                 .child(
                                     Button::new("learn-more", "Learn More")
-                                        .icon(IconName::ArrowUpRight)
                                         .label_size(LabelSize::Small)
-                                        .icon_size(IconSize::XSmall)
-                                        .icon_color(Color::Muted)
+                                        .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall).color(Color::Muted))
                                         .on_click(|_, _, cx| {
                                             cx.open_url("https://zed.dev/docs/languages/python")
                                         }),

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -18,7 +18,7 @@ use project::{
 };
 use proto::toggle_lsp_logs::LogType;
 use std::{any::TypeId, borrow::Cow, sync::Arc};
-use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
+use ui::{Checkbox, ContextMenu, PopoverMenu, ToggleState, prelude::*};
 use util::ResultExt as _;
 use workspace::{
     SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
@@ -969,9 +969,11 @@ impl Render for LspLogToolbarItemView {
                         })
                         .unwrap_or_else(|| "No server selected".into()),
                 )
-                .icon(IconName::ChevronDown)
-                .icon_size(IconSize::Small)
-                .icon_color(Color::Muted),
+                .end_icon(
+                    Icon::new(IconName::ChevronDown)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                ),
             )
             .menu({
                 let log_view = log_view.clone();
@@ -1030,10 +1032,11 @@ impl Render for LspLogToolbarItemView {
             PopoverMenu::new("LspViewSelector")
                 .anchor(Corner::TopLeft)
                 .trigger(
-                    Button::new("language_server_menu_header", label)
-                        .icon(IconName::ChevronDown)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted),
+                    Button::new("language_server_menu_header", label).end_icon(
+                        Icon::new(IconName::ChevronDown)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    ),
                 )
                 .menu(move |window, cx| {
                     let log_toolbar_view = log_toolbar_view.upgrade()?;
@@ -1125,9 +1128,11 @@ impl Render for LspLogToolbarItemView {
                                                 "language_server_trace_level_selector",
                                                 "Trace level",
                                             )
-                                            .icon(IconName::ChevronDown)
-                                            .icon_size(IconSize::Small)
-                                            .icon_color(Color::Muted),
+                                            .end_icon(
+                                                Icon::new(IconName::ChevronDown)
+                                                    .size(IconSize::Small)
+                                                    .color(Color::Muted),
+                                            ),
                                         )
                                         .menu({
                                             let log_view = log_view;
@@ -1193,9 +1198,11 @@ impl Render for LspLogToolbarItemView {
                                                 "language_server_log_level_selector",
                                                 "Log level",
                                             )
-                                            .icon(IconName::ChevronDown)
-                                            .icon_size(IconSize::Small)
-                                            .icon_color(Color::Muted),
+                                            .end_icon(
+                                                Icon::new(IconName::ChevronDown)
+                                                    .size(IconSize::Small)
+                                                    .color(Color::Muted),
+                                            ),
                                         )
                                         .menu({
                                             let log_view = log_view;

crates/onboarding/src/basics_page.rs 🔗

@@ -10,9 +10,8 @@ use theme::{
     ThemeSettings,
 };
 use ui::{
-    Divider, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
-    ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip,
-    prelude::*, rems_from_px,
+    Divider, StatefulInteractiveElement, SwitchField, TintColor, ToggleButtonGroup,
+    ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, prelude::*,
 };
 use vim_mode_setting::VimModeSetting;
 
@@ -477,8 +476,7 @@ fn render_setting_import_button(
         .toggle_state(imported)
         .tab_index(tab_index)
         .when(imported, |this| {
-            this.icon(IconName::Check)
-                .icon_size(IconSize::Small)
+            this.end_icon(Icon::new(IconName::Check).size(IconSize::Small))
                 .color(Color::Success)
         })
         .on_click(move |_, window, cx| {

crates/onboarding/src/multibuffer_hint.rs 🔗

@@ -158,10 +158,11 @@ impl Render for MultibufferHint {
                     )
                     .child(
                         Button::new("open_docs", "Learn More")
-                            .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .icon_position(IconPosition::End)
+                            .end_icon(
+                                Icon::new(IconName::ArrowUpRight)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .on_click(move |_event, _, cx| {
                                 cx.open_url("https://zed.dev/docs/multibuffers")
                             }),

crates/panel/src/panel.rs 🔗

@@ -52,7 +52,6 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
     let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into());
     ui::Button::new(id, label)
         .label_size(ui::LabelSize::Small)
-        .icon_size(ui::IconSize::Small)
         // TODO: Change this once we use on_surface_bg in button_like
         .layer(ui::ElevationIndex::ModalSurface)
         .size(ui::ButtonSize::Compact)

crates/recent_projects/src/disconnected_overlay.rs 🔗

@@ -2,11 +2,7 @@ use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Rende
 use project::project_settings::ProjectSettings;
 use remote::RemoteConnectionOptions;
 use settings::Settings;
-use ui::{
-    Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline,
-    HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
-    ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems,
-};
+use ui::{ElevationIndex, Modal, ModalFooter, ModalHeader, Section, prelude::*};
 use workspace::{
     ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
 };
@@ -207,8 +203,7 @@ impl Render for DisconnectedOverlay {
                                         Button::new("reconnect", "Reconnect")
                                             .style(ButtonStyle::Filled)
                                             .layer(ElevationIndex::ModalSurface)
-                                            .icon(IconName::ArrowCircle)
-                                            .icon_position(IconPosition::Start)
+                                            .start_icon(Icon::new(IconName::ArrowCircle))
                                             .on_click(cx.listener(Self::handle_reconnect)),
                                     )
                                 }),

crates/recent_projects/src/remote_servers.rs 🔗

@@ -2117,8 +2117,10 @@ impl RemoteServerProjects {
                                     .child(
                                         Button::new("learn-more", "Learn More")
                                             .label_size(LabelSize::Small)
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::XSmall)
+                                            .end_icon(
+                                                Icon::new(IconName::ArrowUpRight)
+                                                    .size(IconSize::XSmall),
+                                            )
                                             .on_click(|_, _, cx| {
                                                 cx.open_url(
                                                     "https://zed.dev/docs/remote-development",

crates/repl/src/components/kernel_options.rs 🔗

@@ -431,10 +431,11 @@ impl PickerDelegate for KernelPickerDelegate {
                 .gap_4()
                 .child(
                     Button::new("kernel-docs", "Kernel Docs")
-                        .icon(IconName::ArrowUpRight)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted)
-                        .icon_position(IconPosition::End)
+                        .end_icon(
+                            Icon::new(IconName::ArrowUpRight)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .on_click(move |_, _, cx| cx.open_url(KERNEL_DOCS_URL)),
                 )
                 .into_any(),

crates/repl/src/notebook/notebook_ui.rs 🔗

@@ -1117,10 +1117,11 @@ impl NotebookEditor {
                     worktree_id,
                     Button::new("kernel-selector", kernel_name.clone())
                         .label_size(LabelSize::Small)
-                        .icon(status_icon)
-                        .icon_size(IconSize::Small)
-                        .icon_color(status_color)
-                        .icon_position(IconPosition::Start),
+                        .start_icon(
+                            Icon::new(status_icon)
+                                .size(IconSize::Small)
+                                .color(status_color),
+                        ),
                     Tooltip::text(format!(
                         "Kernel: {} ({}). Click to change.",
                         kernel_name,

crates/rules_library/src/rules_library.rs 🔗

@@ -1170,10 +1170,11 @@ impl RulesLibrary {
                             Button::new("new-rule", "New Rule")
                                 .full_width()
                                 .style(ButtonStyle::Outlined)
-                                .icon(IconName::Plus)
-                                .icon_size(IconSize::Small)
-                                .icon_position(IconPosition::Start)
-                                .icon_color(Color::Muted)
+                                .start_icon(
+                                    Icon::new(IconName::Plus)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                )
                                 .on_click(|_, window, cx| {
                                     window.dispatch_action(Box::new(NewRule), cx);
                                 }),

crates/search/src/project_search.rs 🔗

@@ -1583,9 +1583,7 @@ impl ProjectSearchView {
             )
             .child(
                 Button::new("filter-paths", "Include/exclude specific paths")
-                    .icon(IconName::Filter)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(Icon::new(IconName::Filter).size(IconSize::Small))
                     .key_binding(KeyBinding::for_action_in(&ToggleFilters, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleFilters.boxed_clone(), cx)
@@ -1593,9 +1591,7 @@ impl ProjectSearchView {
             )
             .child(
                 Button::new("find-replace", "Find and replace")
-                    .icon(IconName::Replace)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(Icon::new(IconName::Replace).size(IconSize::Small))
                     .key_binding(KeyBinding::for_action_in(&ToggleReplace, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleReplace.boxed_clone(), cx)
@@ -1603,9 +1599,7 @@ impl ProjectSearchView {
             )
             .child(
                 Button::new("regex", "Match with regex")
-                    .icon(IconName::Regex)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(Icon::new(IconName::Regex).size(IconSize::Small))
                     .key_binding(KeyBinding::for_action_in(&ToggleRegex, &focus_handle, cx))
                     .on_click(|_event, window, cx| {
                         window.dispatch_action(ToggleRegex.boxed_clone(), cx)
@@ -1613,9 +1607,7 @@ impl ProjectSearchView {
             )
             .child(
                 Button::new("match-case", "Match case")
-                    .icon(IconName::CaseSensitive)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(Icon::new(IconName::CaseSensitive).size(IconSize::Small))
                     .key_binding(KeyBinding::for_action_in(
                         &ToggleCaseSensitive,
                         &focus_handle,
@@ -1627,9 +1619,7 @@ impl ProjectSearchView {
             )
             .child(
                 Button::new("match-whole-words", "Match whole words")
-                    .icon(IconName::WholeWord)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
+                    .start_icon(Icon::new(IconName::WholeWord).size(IconSize::Small))
                     .key_binding(KeyBinding::for_action_in(
                         &ToggleWholeWord,
                         &focus_handle,

crates/settings_ui/src/pages/tool_permissions_setup.rs 🔗

@@ -275,10 +275,11 @@ fn render_tool_list_item(
                 .tab_index(tool_index as isize)
                 .style(ButtonStyle::OutlinedGhost)
                 .size(ButtonSize::Medium)
-                .icon(IconName::ChevronRight)
-                .icon_position(IconPosition::End)
-                .icon_color(Color::Muted)
-                .icon_size(IconSize::Small)
+                .end_icon(
+                    Icon::new(IconName::ChevronRight)
+                        .size(IconSize::Small)
+                        .color(Color::Muted),
+                )
                 .on_click(cx.listener(move |this, _, window, cx| {
                     this.push_dynamic_sub_page(
                         tool_name,
@@ -1090,9 +1091,7 @@ fn render_global_default_mode_section(current_mode: ToolPermissionMode) -> AnyEl
                         .tab_index(0_isize)
                         .style(ButtonStyle::Outlined)
                         .size(ButtonSize::Medium)
-                        .icon(IconName::ChevronDown)
-                        .icon_position(IconPosition::End)
-                        .icon_size(IconSize::Small),
+                        .end_icon(Icon::new(IconName::ChevronDown).size(IconSize::Small)),
                 )
                 .menu(move |window, cx| {
                     Some(ContextMenu::build(window, cx, move |menu, _, _| {
@@ -1141,9 +1140,7 @@ fn render_default_mode_section(
                         .tab_index(0_isize)
                         .style(ButtonStyle::Outlined)
                         .size(ButtonSize::Medium)
-                        .icon(IconName::ChevronDown)
-                        .icon_position(IconPosition::End)
-                        .icon_size(IconSize::Small),
+                        .end_icon(Icon::new(IconName::ChevronDown).size(IconSize::Small)),
                 )
                 .menu(move |window, cx| {
                     let tool_id = tool_id_owned.clone();

crates/settings_ui/src/settings_ui.rs 🔗

@@ -925,9 +925,7 @@ impl SettingsPageItem {
                         Button::new("error-warning", warning)
                             .style(ButtonStyle::Outlined)
                             .size(ButtonSize::Medium)
-                            .icon(Some(IconName::Debug))
-                            .icon_position(IconPosition::Start)
-                            .icon_color(Color::Error)
+                            .start_icon(Icon::new(IconName::Debug).color(Color::Error))
                             .tab_index(0_isize)
                             .tooltip(Tooltip::text(setting_item.field.type_name()))
                             .into_any_element(),
@@ -992,11 +990,12 @@ impl SettingsPageItem {
                                 ("sub-page".into(), sub_page_link.title.clone()),
                                 "Configure",
                             )
-                            .icon(IconName::ChevronRight)
                             .tab_index(0_isize)
-                            .icon_position(IconPosition::End)
-                            .icon_color(Color::Muted)
-                            .icon_size(IconSize::Small)
+                            .end_icon(
+                                Icon::new(IconName::ChevronRight)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .style(ButtonStyle::OutlinedGhost)
                             .size(ButtonSize::Medium)
                             .on_click({
@@ -1125,11 +1124,12 @@ impl SettingsPageItem {
                                 ("action-link".into(), action_link.title.clone()),
                                 action_link.button_text.clone(),
                             )
-                            .icon(IconName::ArrowUpRight)
                             .tab_index(0_isize)
-                            .icon_position(IconPosition::End)
-                            .icon_color(Color::Muted)
-                            .icon_size(IconSize::Small)
+                            .end_icon(
+                                Icon::new(IconName::ArrowUpRight)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
                             .style(ButtonStyle::OutlinedGhost)
                             .size(ButtonSize::Medium)
                             .on_click({
@@ -4174,10 +4174,11 @@ fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button
         .tab_index(0_isize)
         .style(ButtonStyle::Outlined)
         .size(ButtonSize::Medium)
-        .icon(IconName::ChevronUpDown)
-        .icon_color(Color::Muted)
-        .icon_size(IconSize::Small)
-        .icon_position(IconPosition::End)
+        .end_icon(
+            Icon::new(IconName::ChevronUpDown)
+                .size(IconSize::Small)
+                .color(Color::Muted),
+        )
 }
 
 fn render_font_picker(

crates/theme_selector/src/icon_theme_selector.rs 🔗

@@ -311,10 +311,11 @@ impl PickerDelegate for IconThemeSelectorDelegate {
                 .border_color(cx.theme().colors().border_variant)
                 .child(
                     Button::new("docs", "View Icon Theme Docs")
-                        .icon(IconName::ArrowUpRight)
-                        .icon_position(IconPosition::End)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted)
+                        .end_icon(
+                            Icon::new(IconName::ArrowUpRight)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .on_click(|_event, _window, cx| {
                             cx.open_url("https://zed.dev/docs/icon-themes");
                         }),

crates/theme_selector/src/theme_selector.rs 🔗

@@ -497,10 +497,11 @@ impl PickerDelegate for ThemeSelectorDelegate {
                 .border_color(cx.theme().colors().border_variant)
                 .child(
                     Button::new("docs", "View Theme Docs")
-                        .icon(IconName::ArrowUpRight)
-                        .icon_position(IconPosition::End)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted)
+                        .end_icon(
+                            Icon::new(IconName::ArrowUpRight)
+                                .size(IconSize::Small)
+                                .color(Color::Muted),
+                        )
                         .on_click(cx.listener(|_, _, _, cx| {
                             cx.open_url("https://zed.dev/docs/themes");
                         })),

crates/title_bar/src/title_bar.rs 🔗

@@ -583,10 +583,11 @@ impl TitleBar {
             .style(ButtonStyle::Tinted(TintColor::Warning))
             .label_size(LabelSize::Small)
             .color(Color::Warning)
-            .icon(IconName::Warning)
-            .icon_color(Color::Warning)
-            .icon_size(IconSize::Small)
-            .icon_position(IconPosition::Start)
+            .start_icon(
+                Icon::new(IconName::Warning)
+                    .size(IconSize::Small)
+                    .color(Color::Warning),
+            )
             .tooltip(|_, cx| {
                 Tooltip::with_meta(
                     "You're in Restricted Mode",
@@ -697,9 +698,11 @@ impl TitleBar {
                 Button::new("project_name_trigger", display_name)
                     .label_size(LabelSize::Small)
                     .when(self.worktree_count(cx) > 1, |this| {
-                        this.icon(IconName::ChevronDown)
-                            .icon_color(Color::Muted)
-                            .icon_size(IconSize::XSmall)
+                        this.end_icon(
+                            Icon::new(IconName::ChevronDown)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
                     })
                     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                     .when(!is_project_selected, |s| s.color(Color::Muted)),
@@ -779,11 +782,9 @@ impl TitleBar {
                         .color(Color::Muted)
                         .when(settings.show_branch_icon, |branch_button| {
                             let (icon, icon_color) = icon_info;
-                            branch_button
-                                .icon(icon)
-                                .icon_position(IconPosition::Start)
-                                .icon_color(icon_color)
-                                .icon_size(IconSize::Indicator)
+                            branch_button.start_icon(
+                                Icon::new(icon).size(IconSize::Indicator).color(icon_color),
+                            )
                         }),
                     move |_window, cx| {
                         Tooltip::with_meta(

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

@@ -133,10 +133,11 @@ impl RenderOnce for ConfiguredApiCard {
                         elem.tab_index(tab_index)
                     })
                     .label_size(LabelSize::Small)
-                    .icon(IconName::Undo)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
-                    .icon_position(IconPosition::Start)
+                    .start_icon(
+                        Icon::new(IconName::Undo)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
                     .disabled(self.disabled)
                     .when_some(self.tooltip_label, |this, label| {
                         this.tooltip(Tooltip::text(label))

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

@@ -8,16 +8,14 @@ use gpui::{AnyElement, IntoElement, ParentElement, Styled};
 ///
 /// ```
 /// use ui::prelude::*;
-/// use ui::{Banner, Button, IconName, IconPosition, IconSize, Label, Severity};
+/// use ui::{Banner, Button, Icon, IconName, IconSize, Label, Severity};
 ///
 /// Banner::new()
 ///     .severity(Severity::Success)
 ///     .children([Label::new("This is a success message")])
 ///     .action_slot(
 ///         Button::new("learn-more", "Learn More")
-///             .icon(IconName::ArrowUpRight)
-///             .icon_size(IconSize::Small)
-///             .icon_position(IconPosition::End)
+///             .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)),
 ///     );
 /// ```
 #[derive(IntoElement, RegisterComponent)]
@@ -151,9 +149,7 @@ impl Component for Banner {
                     .child(Label::new("This is an informational message"))
                     .action_slot(
                         Button::new("learn-more", "Learn More")
-                            .icon(IconName::ArrowUpRight)
-                            .icon_size(IconSize::Small)
-                            .icon_position(IconPosition::End),
+                            .end_icon(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)),
                     )
                     .into_any_element(),
             ),

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

@@ -2,15 +2,12 @@ use crate::component_prelude::*;
 use gpui::{AnyElement, AnyView, DefiniteLength};
 use ui_macros::RegisterComponent;
 
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, Label};
 use crate::{
-    Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, KeybindingPosition, TintColor,
-    prelude::*,
+    Color, DynamicSpacing, ElevationIndex, KeyBinding, KeybindingPosition, TintColor, prelude::*,
 };
 
-use super::button_icon::ButtonIcon;
-
-/// An element that creates a button with a label and an optional icon.
+/// An element that creates a button with a label and optional icons.
 ///
 /// Common buttons:
 /// - Label, Icon + Label: [`Button`] (this component)
@@ -42,7 +39,7 @@ use super::button_icon::ButtonIcon;
 /// use ui::prelude::*;
 ///
 /// Button::new("button_id", "Click me!")
-///     .icon(IconName::Check)
+///     .start_icon(Icon::new(IconName::Check))
 ///     .toggle_state(true)
 ///     .on_click(|event, window, cx| {
 ///         // Handle click event
@@ -85,12 +82,8 @@ pub struct Button {
     label_size: Option<LabelSize>,
     selected_label: Option<SharedString>,
     selected_label_color: Option<Color>,
-    icon: Option<IconName>,
-    icon_position: Option<IconPosition>,
-    icon_size: Option<IconSize>,
-    icon_color: Option<Color>,
-    selected_icon: Option<IconName>,
-    selected_icon_color: Option<Color>,
+    start_icon: Option<Icon>,
+    end_icon: Option<Icon>,
     key_binding: Option<KeyBinding>,
     key_binding_position: KeybindingPosition,
     alpha: Option<f32>,
@@ -112,12 +105,8 @@ impl Button {
             label_size: None,
             selected_label: None,
             selected_label_color: None,
-            icon: None,
-            icon_position: None,
-            icon_size: None,
-            icon_color: None,
-            selected_icon: None,
-            selected_icon_color: None,
+            start_icon: None,
+            end_icon: None,
             key_binding: None,
             key_binding_position: KeybindingPosition::default(),
             alpha: None,
@@ -149,39 +138,19 @@ impl Button {
         self
     }
 
-    /// Assigns an icon to the button.
-    pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
-        self.icon = icon.into();
-        self
-    }
-
-    /// Sets the position of the icon relative to the label.
-    pub fn icon_position(mut self, icon_position: impl Into<Option<IconPosition>>) -> Self {
-        self.icon_position = icon_position.into();
-        self
-    }
-
-    /// Specifies the size of the button's icon.
-    pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
-        self.icon_size = icon_size.into();
-        self
-    }
-
-    /// Sets the color of the button's icon.
-    pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
-        self.icon_color = icon_color.into();
-        self
-    }
-
-    /// Chooses an icon to display when the button is in a selected state.
-    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
-        self.selected_icon = icon.into();
+    /// Sets an icon to display at the start (left) of the button label.
+    ///
+    /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
+    pub fn start_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.start_icon = icon.into();
         self
     }
 
-    /// Sets the icon color used when the button is in a selected state.
-    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
-        self.selected_icon_color = color.into();
+    /// Sets an icon to display at the end (right) of the button label.
+    ///
+    /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
+    pub fn end_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+        self.end_icon = icon.into();
         self
     }
 
@@ -219,22 +188,24 @@ impl Button {
 impl Toggleable for Button {
     /// Sets the selected state of the button.
     ///
-    /// This method allows the selection state of the button to be specified.
-    /// It modifies the button's appearance to reflect its selected state.
-    ///
     /// # Examples
     ///
+    /// Create a toggleable button that changes appearance when selected:
+    ///
     /// ```
     /// use ui::prelude::*;
+    /// use ui::TintColor;
     ///
-    /// Button::new("button_id", "Click me!")
-    ///     .toggle_state(true)
+    /// let selected = true;
+    ///
+    /// Button::new("toggle_button", "Toggle Me")
+    ///     .start_icon(Icon::new(IconName::Check))
+    ///     .toggle_state(selected)
+    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
     ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
+    ///         // Toggle the selected state
     ///     });
     /// ```
-    ///
-    /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
     fn toggle_state(mut self, selected: bool) -> Self {
         self.base = self.base.toggle_state(selected);
         self
@@ -242,22 +213,20 @@ impl Toggleable for Button {
 }
 
 impl SelectableButton for Button {
-    /// Sets the style for the button when selected.
+    /// Sets the style for the button in a selected state.
     ///
     /// # Examples
     ///
+    /// Customize the selected appearance of a button:
+    ///
     /// ```
     /// use ui::prelude::*;
     /// use ui::TintColor;
     ///
-    /// Button::new("button_id", "Click me!")
+    /// Button::new("styled_button", "Styled Button")
     ///     .toggle_state(true)
-    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-    ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
-    ///     });
+    ///     .selected_style(ButtonStyle::Tinted(TintColor::Accent));
     /// ```
-    /// This results in a button with a blue tinted background when selected.
     fn selected_style(mut self, style: ButtonStyle) -> Self {
         self.base = self.base.selected_style(style);
         self
@@ -265,36 +234,27 @@ impl SelectableButton for Button {
 }
 
 impl Disableable for Button {
-    /// Disables the button.
+    /// Disables the button, preventing interaction and changing its appearance.
     ///
-    /// This method allows the button to be disabled. When a button is disabled,
-    /// it doesn't react to user interactions and its appearance is updated to reflect this.
+    /// When disabled, the button's icon and label will use `Color::Disabled`.
     ///
     /// # Examples
     ///
+    /// Create a disabled button:
+    ///
     /// ```
     /// use ui::prelude::*;
     ///
-    /// Button::new("button_id", "Click me!")
-    ///     .disabled(true)
-    ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
-    ///     });
+    /// Button::new("disabled_button", "Can't Click Me")
+    ///     .disabled(true);
     /// ```
-    ///
-    /// This results in a button that is disabled and does not respond to click events.
     fn disabled(mut self, disabled: bool) -> Self {
         self.base = self.base.disabled(disabled);
-        self.key_binding = self
-            .key_binding
-            .take()
-            .map(|binding| binding.disabled(disabled));
         self
     }
 }
 
 impl Clickable for Button {
-    /// Sets the click event handler for the button.
     fn on_click(
         mut self,
         handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
@@ -310,44 +270,35 @@ impl Clickable for Button {
 }
 
 impl FixedWidth for Button {
-    /// Sets a fixed width for the button.
-    ///
-    /// This function allows a button to have a fixed width instead of automatically growing or shrinking.
     /// Sets a fixed width for the button.
     ///
     /// # Examples
     ///
+    /// Create a button with a fixed width of 100 pixels:
+    ///
     /// ```
     /// use ui::prelude::*;
     ///
-    /// Button::new("button_id", "Click me!")
-    ///     .width(px(100.))
-    ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
-    ///     });
+    /// Button::new("fixed_width_button", "Fixed Width")
+    ///     .width(px(100.0));
     /// ```
-    ///
-    /// This sets the button's width to be exactly 100 pixels.
     fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
         self.base = self.base.width(width);
         self
     }
 
-    /// Sets the button to occupy the full width of its container.
+    /// Makes the button take up the full width of its container.
     ///
     /// # Examples
     ///
+    /// Create a button that takes up the full width of its container:
+    ///
     /// ```
     /// use ui::prelude::*;
     ///
-    /// Button::new("button_id", "Click me!")
-    ///     .full_width()
-    ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
-    ///     });
+    /// Button::new("full_width_button", "Full Width")
+    ///     .full_width();
     /// ```
-    ///
-    /// This stretches the button to the full width of its container.
     fn full_width(mut self) -> Self {
         self.base = self.base.full_width();
         self
@@ -355,43 +306,34 @@ impl FixedWidth for Button {
 }
 
 impl ButtonCommon for Button {
-    /// Sets the button's id.
     fn id(&self) -> &ElementId {
         self.base.id()
     }
 
-    /// Sets the visual style of the button using a [`ButtonStyle`].
+    /// Sets the visual style of the button.
     fn style(mut self, style: ButtonStyle) -> Self {
         self.base = self.base.style(style);
         self
     }
 
-    /// Sets the button's size using a [`ButtonSize`].
+    /// Sets the size of the button.
     fn size(mut self, size: ButtonSize) -> Self {
         self.base = self.base.size(size);
         self
     }
 
-    /// Sets a tooltip for the button.
-    ///
-    /// This method allows a tooltip to be set for the button. The tooltip is a function that
-    /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The
-    /// tooltip is displayed when the user hovers over the button.
+    /// Sets a tooltip that appears on hover.
     ///
     /// # Examples
     ///
-    /// ```
-    /// use ui::prelude::*;
-    /// use ui::Tooltip;
+    /// Add a tooltip to a button:
     ///
-    /// Button::new("button_id", "Click me!")
-    ///     .tooltip(Tooltip::text("This is a tooltip"))
-    ///     .on_click(|event, window, cx| {
-    ///         // Handle click event
-    ///     });
     /// ```
+    /// use ui::{Tooltip, prelude::*};
     ///
-    /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over.
+    /// Button::new("tooltip_button", "Hover Me")
+    ///     .tooltip(Tooltip::text("This is a tooltip"));
+    /// ```
     fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
         self.base = self.base.tooltip(tooltip);
         self
@@ -436,16 +378,12 @@ impl RenderOnce for Button {
             h_flex()
                 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
                 .gap(DynamicSpacing::Base04.rems(cx))
-                .when(self.icon_position == Some(IconPosition::Start), |this| {
-                    this.children(self.icon.map(|icon| {
-                        ButtonIcon::new(icon)
-                            .disabled(is_disabled)
-                            .toggle_state(is_selected)
-                            .selected_icon(self.selected_icon)
-                            .selected_icon_color(self.selected_icon_color)
-                            .size(self.icon_size)
-                            .color(self.icon_color)
-                    }))
+                .when_some(self.start_icon, |this, icon| {
+                    this.child(if is_disabled {
+                        icon.color(Color::Disabled)
+                    } else {
+                        icon
+                    })
                 })
                 .child(
                     h_flex()
@@ -465,16 +403,12 @@ impl RenderOnce for Button {
                         )
                         .children(self.key_binding),
                 )
-                .when(self.icon_position != Some(IconPosition::Start), |this| {
-                    this.children(self.icon.map(|icon| {
-                        ButtonIcon::new(icon)
-                            .disabled(is_disabled)
-                            .toggle_state(is_selected)
-                            .selected_icon(self.selected_icon)
-                            .selected_icon_color(self.selected_icon_color)
-                            .size(self.icon_size)
-                            .color(self.icon_color)
-                    }))
+                .when_some(self.end_icon, |this, icon| {
+                    this.child(if is_disabled {
+                        icon.color(Color::Disabled)
+                    } else {
+                        icon
+                    })
                 }),
         )
     }
@@ -585,24 +519,28 @@ impl Component for Button {
                         "Buttons with Icons",
                         vec![
                             single_example(
-                                "Icon Start",
-                                Button::new("icon_start", "Icon Start")
-                                    .icon(IconName::Check)
-                                    .icon_position(IconPosition::Start)
+                                "Start Icon",
+                                Button::new("icon_start", "Start Icon")
+                                    .start_icon(Icon::new(IconName::Check))
+                                    .into_any_element(),
+                            ),
+                            single_example(
+                                "End Icon",
+                                Button::new("icon_end", "End Icon")
+                                    .end_icon(Icon::new(IconName::Check))
                                     .into_any_element(),
                             ),
                             single_example(
-                                "Icon End",
-                                Button::new("icon_end", "Icon End")
-                                    .icon(IconName::Check)
-                                    .icon_position(IconPosition::End)
+                                "Both Icons",
+                                Button::new("both_icons", "Both Icons")
+                                    .start_icon(Icon::new(IconName::Check))
+                                    .end_icon(Icon::new(IconName::ChevronDown))
                                     .into_any_element(),
                             ),
                             single_example(
                                 "Icon Color",
                                 Button::new("icon_color", "Icon Color")
-                                    .icon(IconName::Check)
-                                    .icon_color(Color::Accent)
+                                    .start_icon(Icon::new(IconName::Check).color(Color::Accent))
                                     .into_any_element(),
                             ),
                         ],

crates/ui/src/components/button/button_icon.rs 🔗

@@ -1,199 +0,0 @@
-use crate::{Icon, IconName, IconSize, IconWithIndicator, Indicator, prelude::*};
-use gpui::Hsla;
-
-/// An icon that appears within a button.
-///
-/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
-/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
-#[derive(IntoElement, RegisterComponent)]
-pub(super) struct ButtonIcon {
-    icon: IconName,
-    size: IconSize,
-    color: Color,
-    disabled: bool,
-    selected: bool,
-    selected_icon: Option<IconName>,
-    selected_icon_color: Option<Color>,
-    selected_style: Option<ButtonStyle>,
-    indicator: Option<Indicator>,
-    indicator_border_color: Option<Hsla>,
-}
-
-impl ButtonIcon {
-    pub fn new(icon: IconName) -> Self {
-        Self {
-            icon,
-            size: IconSize::default(),
-            color: Color::default(),
-            disabled: false,
-            selected: false,
-            selected_icon: None,
-            selected_icon_color: None,
-            selected_style: None,
-            indicator: None,
-            indicator_border_color: None,
-        }
-    }
-
-    pub fn size(mut self, size: impl Into<Option<IconSize>>) -> Self {
-        if let Some(size) = size.into() {
-            self.size = size;
-        }
-        self
-    }
-
-    pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
-        if let Some(color) = color.into() {
-            self.color = color;
-        }
-        self
-    }
-
-    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
-        self.selected_icon = icon.into();
-        self
-    }
-
-    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
-        self.selected_icon_color = color.into();
-        self
-    }
-
-    pub fn indicator(mut self, indicator: Indicator) -> Self {
-        self.indicator = Some(indicator);
-        self
-    }
-
-    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
-        self.indicator_border_color = color;
-        self
-    }
-}
-
-impl Disableable for ButtonIcon {
-    fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-}
-
-impl Toggleable for ButtonIcon {
-    fn toggle_state(mut self, selected: bool) -> Self {
-        self.selected = selected;
-        self
-    }
-}
-
-impl SelectableButton for ButtonIcon {
-    fn selected_style(mut self, style: ButtonStyle) -> Self {
-        self.selected_style = Some(style);
-        self
-    }
-}
-
-impl RenderOnce for ButtonIcon {
-    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let icon = self
-            .selected_icon
-            .filter(|_| self.selected)
-            .unwrap_or(self.icon);
-
-        let icon_color = if self.disabled {
-            Color::Disabled
-        } else if self.selected_style.is_some() && self.selected {
-            self.selected_style.unwrap().into()
-        } else if self.selected {
-            self.selected_icon_color.unwrap_or(Color::Selected)
-        } else {
-            self.color
-        };
-
-        let icon = Icon::new(icon).size(self.size).color(icon_color);
-
-        match self.indicator {
-            Some(indicator) => IconWithIndicator::new(icon, Some(indicator))
-                .indicator_border_color(self.indicator_border_color)
-                .into_any_element(),
-            None => icon.into_any_element(),
-        }
-    }
-}
-
-impl Component for ButtonIcon {
-    fn scope() -> ComponentScope {
-        ComponentScope::Input
-    }
-
-    fn name() -> &'static str {
-        "ButtonIcon"
-    }
-
-    fn description() -> Option<&'static str> {
-        Some("An icon component specifically designed for use within buttons.")
-    }
-
-    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
-        Some(
-            v_flex()
-                .gap_6()
-                .children(vec![
-                    example_group_with_title(
-                        "Basic Usage",
-                        vec![
-                            single_example(
-                                "Default",
-                                ButtonIcon::new(IconName::Star).into_any_element(),
-                            ),
-                            single_example(
-                                "Custom Size",
-                                ButtonIcon::new(IconName::Star)
-                                    .size(IconSize::Medium)
-                                    .into_any_element(),
-                            ),
-                            single_example(
-                                "Custom Color",
-                                ButtonIcon::new(IconName::Star)
-                                    .color(Color::Accent)
-                                    .into_any_element(),
-                            ),
-                        ],
-                    ),
-                    example_group_with_title(
-                        "States",
-                        vec![
-                            single_example(
-                                "Selected",
-                                ButtonIcon::new(IconName::Star)
-                                    .toggle_state(true)
-                                    .into_any_element(),
-                            ),
-                            single_example(
-                                "Disabled",
-                                ButtonIcon::new(IconName::Star)
-                                    .disabled(true)
-                                    .into_any_element(),
-                            ),
-                        ],
-                    ),
-                    example_group_with_title(
-                        "With Indicator",
-                        vec![
-                            single_example(
-                                "Default Indicator",
-                                ButtonIcon::new(IconName::Star)
-                                    .indicator(Indicator::dot())
-                                    .into_any_element(),
-                            ),
-                            single_example(
-                                "Custom Indicator",
-                                ButtonIcon::new(IconName::Star)
-                                    .indicator(Indicator::dot().color(Color::Error))
-                                    .into_any_element(),
-                            ),
-                        ],
-                    ),
-                ])
-                .into_any_element(),
-        )
-    }
-}

crates/ui/src/components/button/icon_button.rs 🔗

@@ -1,11 +1,11 @@
 use gpui::{AnyView, DefiniteLength, Hsla};
 
 use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
-use crate::{ElevationIndex, Indicator, SelectableButton, TintColor, prelude::*};
+use crate::{
+    ElevationIndex, Icon, IconWithIndicator, Indicator, SelectableButton, TintColor, prelude::*,
+};
 use crate::{IconName, IconSize};
 
-use super::button_icon::ButtonIcon;
-
 /// The shape of an [`IconButton`].
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 pub enum IconButtonShape {
@@ -22,6 +22,7 @@ pub struct IconButton {
     icon_color: Color,
     selected_icon: Option<IconName>,
     selected_icon_color: Option<Color>,
+    selected_style: Option<ButtonStyle>,
     indicator: Option<Indicator>,
     indicator_border_color: Option<Hsla>,
     alpha: Option<f32>,
@@ -37,6 +38,7 @@ impl IconButton {
             icon_color: Color::Default,
             selected_icon: None,
             selected_icon_color: None,
+            selected_style: None,
             indicator: None,
             indicator_border_color: None,
             alpha: None,
@@ -112,6 +114,7 @@ impl Toggleable for IconButton {
 
 impl SelectableButton for IconButton {
     fn selected_style(mut self, style: ButtonStyle) -> Self {
+        self.selected_style = Some(style);
         self.base = self.base.selected_style(style);
         self
     }
@@ -192,9 +195,25 @@ impl RenderOnce for IconButton {
     fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike {
         let is_disabled = self.base.disabled;
         let is_selected = self.base.selected;
-        let selected_style = self.base.selected_style;
 
-        let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0));
+        let icon = self
+            .selected_icon
+            .filter(|_| is_selected)
+            .unwrap_or(self.icon);
+
+        let icon_color = if is_disabled {
+            Color::Disabled
+        } else if self.selected_style.is_some() && is_selected {
+            self.selected_style.unwrap().into()
+        } else if is_selected {
+            self.selected_icon_color.unwrap_or(Color::Selected)
+        } else {
+            let base_color = self.icon_color.color(cx);
+            Color::Custom(base_color.opacity(self.alpha.unwrap_or(1.0)))
+        };
+
+        let icon_element = Icon::new(icon).size(self.icon_size).color(icon_color);
+
         self.base
             .map(|this| match self.shape {
                 IconButtonShape::Square => {
@@ -203,20 +222,12 @@ impl RenderOnce for IconButton {
                 }
                 IconButtonShape::Wide => this,
             })
-            .child(
-                ButtonIcon::new(self.icon)
-                    .disabled(is_disabled)
-                    .toggle_state(is_selected)
-                    .selected_icon(self.selected_icon)
-                    .selected_icon_color(self.selected_icon_color)
-                    .when_some(selected_style, |this, style| this.selected_style(style))
-                    .when_some(self.indicator, |this, indicator| {
-                        this.indicator(indicator)
-                            .indicator_border_color(self.indicator_border_color)
-                    })
-                    .size(self.icon_size)
-                    .color(Color::Custom(color)),
-            )
+            .child(match self.indicator {
+                Some(indicator) => IconWithIndicator::new(icon_element, Some(indicator))
+                    .indicator_border_color(self.indicator_border_color)
+                    .into_any_element(),
+                None => icon_element.into_any_element(),
+            })
     }
 }
 

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

@@ -163,11 +163,10 @@ impl RenderOnce for DropdownMenu {
                 Some(
                     Button::new(self.id.clone(), text)
                         .style(button_style)
-                        .when(self.chevron, |this| {
-                            this.icon(self.trigger_icon)
-                                .icon_position(IconPosition::End)
-                                .icon_size(IconSize::XSmall)
-                                .icon_color(Color::Muted)
+                        .when_some(self.trigger_icon.filter(|_| self.chevron), |this, icon| {
+                            this.end_icon(
+                                Icon::new(icon).size(IconSize::XSmall).color(Color::Muted),
+                            )
                         })
                         .when(full_width, |this| this.full_width())
                         .size(trigger_size)

crates/workspace/src/notifications.rs 🔗

@@ -917,11 +917,11 @@ pub mod simple_message_notification {
                                 }));
 
                             if let Some(icon) = self.primary_icon {
-                                button = button
-                                    .icon(icon)
-                                    .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
-                                    .icon_position(IconPosition::Start)
-                                    .icon_size(IconSize::Small);
+                                button = button.start_icon(
+                                    Icon::new(icon)
+                                        .size(IconSize::Small)
+                                        .color(self.primary_icon_color.unwrap_or(Color::Muted)),
+                                );
                             }
 
                             button
@@ -937,11 +937,11 @@ pub mod simple_message_notification {
                                 }));
 
                             if let Some(icon) = self.secondary_icon {
-                                button = button
-                                    .icon(icon)
-                                    .icon_position(IconPosition::Start)
-                                    .icon_size(IconSize::Small)
-                                    .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
+                                button = button.start_icon(
+                                    Icon::new(icon)
+                                        .size(IconSize::Small)
+                                        .color(self.secondary_icon_color.unwrap_or(Color::Muted)),
+                                );
                             }
 
                             button
@@ -955,9 +955,11 @@ pub mod simple_message_notification {
                                         let url = url.clone();
                                         Button::new(message.clone(), message.clone())
                                             .label_size(LabelSize::Small)
-                                            .icon(IconName::ArrowUpRight)
-                                            .icon_size(IconSize::Indicator)
-                                            .icon_color(Color::Muted)
+                                            .end_icon(
+                                                Icon::new(IconName::ArrowUpRight)
+                                                    .size(IconSize::Indicator)
+                                                    .color(Color::Muted),
+                                            )
                                             .on_click(cx.listener(move |_, _, _, cx| {
                                                 cx.open_url(&url);
                                             }))