git_ui: Commit modal refinement (#25484)

Nate Butler and Mikayla Maki created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

assets/keymaps/default-linux.json                     |   8 
assets/keymaps/default-macos.json                     |  17 
crates/assistant_context_editor/src/context_editor.rs |   4 
crates/extensions_ui/src/components/extension_card.rs |   5 
crates/git_ui/src/commit_modal.rs                     | 321 +++++++++++-
crates/git_ui/src/git_panel.rs                        |  58 ++
crates/git_ui/src/quick_commit.rs                     |   0 
crates/gpui/src/color.rs                              |  55 ++
crates/outline/src/outline.rs                         |   4 
crates/panel/src/panel.rs                             |   1 
crates/theme/src/theme.rs                             |   8 
crates/ui/src/components/keybinding_hint.rs           | 131 +---
crates/ui/src/styles/elevation.rs                     |  36 +
crates/ui/src/traits/styled_ext.rs                    |   4 
14 files changed, 508 insertions(+), 144 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -742,6 +742,14 @@
       "alt-up": "git_panel::FocusChanges"
     }
   },
+  {
+    "context": "GitCommit > Editor",
+    "use_key_equivalents": true,
+    "bindings": {
+      "enter": "editor::Newline",
+      "ctrl-enter": "git::Commit"
+    }
+  },
   {
     "context": "CollabPanel && not_editing",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -157,7 +157,8 @@
       "cmd->": "assistant::QuoteSelection",
       "cmd-<": "assistant::InsertIntoEditor",
       "cmd-alt-e": "editor::SelectEnclosingSymbol",
-      "alt-enter": "editor::OpenSelectionsInMultibuffer"
+      "alt-enter": "editor::OpenSelectionsInMultibuffer",
+      "cmd-g": "git::Commit"
     }
   },
   {
@@ -743,22 +744,22 @@
     }
   },
   {
-    "context": "GitCommit > Editor",
+    "context": "GitPanel > Editor",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::Newline",
-      "cmd-enter": "git::Commit"
+      "cmd-enter": "git::Commit",
+      "tab": "git_panel::FocusChanges",
+      "shift-tab": "git_panel::FocusChanges",
+      "alt-up": "git_panel::FocusChanges"
     }
   },
   {
-    "context": "GitPanel > Editor",
+    "context": "GitCommit > Editor",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::Newline",
-      "cmd-enter": "git::Commit",
-      "tab": "git_panel::FocusChanges",
-      "shift-tab": "git_panel::FocusChanges",
-      "alt-up": "git_panel::FocusChanges"
+      "cmd-enter": "git::Commit"
     }
   },
   {

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -1234,8 +1234,8 @@ impl ContextEditor {
                     .px_1()
                     .mr_0p5()
                     .border_1()
-                    .border_color(theme::color_alpha(colors.border_variant, 0.6))
-                    .bg(theme::color_alpha(colors.element_background, 0.6))
+                    .border_color(colors.border_variant.alpha(0.6))
+                    .bg(colors.element_background.alpha(0.6))
                     .child("esc"),
             )
             .child("to cancel")

crates/extensions_ui/src/components/extension_card.rs 🔗

@@ -53,10 +53,7 @@ impl RenderOnce for ExtensionCard {
                             .size_full()
                             .items_center()
                             .justify_center()
-                            .bg(theme::color_alpha(
-                                cx.theme().colors().elevated_surface_background,
-                                0.8,
-                            ))
+                            .bg(cx.theme().colors().elevated_surface_background.alpha(0.8))
                             .child(Label::new("Overridden by dev extension.")),
                     )
                 }),

crates/git_ui/src/commit_modal.rs 🔗

@@ -4,13 +4,17 @@ use crate::git_panel::{commit_message_editor, GitPanel};
 use crate::repository_selector::RepositorySelector;
 use anyhow::Result;
 use git::Commit;
+use language::language_settings::LanguageSettings;
 use language::Buffer;
-use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
+use panel::{
+    panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
+    panel_icon_button,
+};
 use settings::Settings;
 use theme::ThemeSettings;
-use ui::{prelude::*, Tooltip};
+use ui::{prelude::*, KeybindingHint, Tooltip};
 
-use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
+use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
 use gpui::*;
 use project::git::Repository;
 use project::{Fs, Project};
@@ -18,6 +22,8 @@ use std::sync::Arc;
 use workspace::dock::{Dock, DockPosition, PanelHandle};
 use workspace::{ModalView, Workspace};
 
+// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
+
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, window, cx| {
         let Some(window) = window else {
@@ -32,6 +38,8 @@ pub struct CommitModal {
     git_panel: Entity<GitPanel>,
     commit_editor: Entity<Editor>,
     restore_dock: RestoreDock,
+    current_suggestion: Option<usize>,
+    suggested_messages: Vec<SharedString>,
 }
 
 impl Focusable for CommitModal {
@@ -114,6 +122,7 @@ impl CommitModal {
         cx: &mut Context<Self>,
     ) -> Self {
         let panel = git_panel.read(cx);
+        let suggested_message = panel.suggest_commit_message();
 
         let commit_editor = git_panel.update(cx, |git_panel, cx| {
             git_panel.set_modal_open(true, cx);
@@ -122,36 +131,276 @@ impl CommitModal {
             cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
         });
 
+        let commit_message = commit_editor.read(cx).text(cx);
+
+        if let Some(suggested_message) = suggested_message {
+            if commit_message.is_empty() {
+                commit_editor.update(cx, |editor, cx| {
+                    editor.set_text(suggested_message, window, cx);
+                    editor.select_all(&Default::default(), window, cx);
+                });
+            } else {
+                if commit_message.as_str().trim() == suggested_message.trim() {
+                    commit_editor.update(cx, |editor, cx| {
+                        // select the message to make it easy to delete
+                        editor.select_all(&Default::default(), window, cx);
+                    });
+                }
+            }
+        }
+
         Self {
             git_panel,
             commit_editor,
             restore_dock,
+            current_suggestion: None,
+            suggested_messages: vec![],
         }
     }
 
+    /// Returns container `(width, x padding, border radius)`
+    fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
+        // TODO: Let's set the width based on your set wrap guide if possible
+
+        // let settings = EditorSettings::get_global(cx);
+
+        // let first_wrap_guide = self
+        //     .commit_editor
+        //     .read(cx)
+        //     .wrap_guides(cx)
+        //     .iter()
+        //     .next()
+        //     .map(|(guide, active)| if *active { Some(*guide) } else { None })
+        //     .flatten();
+
+        // let preferred_width = if let Some(guide) = first_wrap_guide {
+        //     guide
+        // } else {
+        //     80
+        // };
+
+        let border_radius = 16.0;
+
+        let preferred_width = 50; // (chars wide)
+
+        let mut width = 460.0;
+        let padding_x = 16.0;
+
+        let mut snapshot = self
+            .commit_editor
+            .update(cx, |editor, cx| editor.snapshot(window, cx));
+        let style = window.text_style().clone();
+
+        let font_id = window.text_system().resolve_font(&style.font());
+        let font_size = style.font_size.to_pixels(window.rem_size());
+        let line_height = style.line_height_in_pixels(window.rem_size());
+        if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
+            width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
+            cx.notify();
+        }
+
+        // cx.notify();
+
+        (width, padding_x, border_radius)
+    }
+
+    // fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
+    //     let new_index = match direction {
+    //         Direction::Next => {
+    //             (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
+    //         }
+    //         Direction::Prev => {
+    //             (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
+    //                 .rem_euclid(self.suggested_messages.len())
+    //         }
+    //     };
+    //     self.current_suggestion = Some(new_index);
+
+    //     cx.notify();
+    // }
+
+    // fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
+    //     self.current_suggestion = Some(1);
+    //     self.apply_suggestion(window, cx);
+    // }
+
+    // fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
+    //     self.current_suggestion = Some(0);
+    //     self.apply_suggestion(window, cx);
+    // }
+
+    // fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
+    //     self.commit_editor.update(cx, |editor, cx| {
+    //         editor.set_text(message.to_string(), window, cx)
+    //     });
+    //     self.current_suggestion = Some(0);
+    //     cx.notify();
+    // }
+
+    // fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+    //     let suggested_messages = self.suggested_messages.clone();
+
+    //     if let Some(suggestion) = self.current_suggestion {
+    //         let suggested_message = &suggested_messages[suggestion];
+
+    //         self.set_commit_message(suggested_message, window, cx);
+    //     }
+
+    //     cx.notify();
+    // }
+
+    fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
+        let mut editor = self.commit_editor.clone();
+
+        let editor_style = panel_editor_style(true, window, cx);
+
+        EditorElement::new(&self.commit_editor, editor_style)
+    }
+
     pub fn render_commit_editor(
         &self,
         name_and_email: Option<(SharedString, SharedString)>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let editor = self.commit_editor.clone();
+        let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
+
+        let border_radius = modal_border_radius - padding_x / 2.0;
 
-        let panel_editor_style = panel_editor_style(true, window, cx);
+        let editor = self.commit_editor.clone();
+        let editor_focus_handle = editor.focus_handle(cx);
 
         let settings = ThemeSettings::get_global(cx);
         let line_height = relative(settings.buffer_line_height.value())
             .to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
 
+        let mut snapshot = self
+            .commit_editor
+            .update(cx, |editor, cx| editor.snapshot(window, cx));
+        let style = window.text_style().clone();
+
+        let font_id = window.text_system().resolve_font(&style.font());
+        let font_size = style.font_size.to_pixels(window.rem_size());
+        let line_height = style.line_height_in_pixels(window.rem_size());
+        let em_width = window.text_system().em_width(font_id, font_size);
+
+        let (branch, tooltip, commit_label, co_authors) =
+            self.git_panel.update(cx, |git_panel, cx| {
+                let branch = git_panel
+                    .active_repository
+                    .as_ref()
+                    .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
+                    .unwrap_or_else(|| "<no branch>".into());
+                let tooltip = if git_panel.has_staged_changes() {
+                    "Commit staged changes"
+                } else {
+                    "Commit changes to tracked files"
+                };
+                let title = if git_panel.has_staged_changes() {
+                    "Commit"
+                } else {
+                    "Commit Tracked"
+                };
+                let co_authors = git_panel.render_co_authors(cx);
+                (branch, tooltip, title, co_authors)
+            });
+
+        let branch_selector = panel_button(branch)
+            .icon(IconName::GitBranch)
+            .icon_size(IconSize::Small)
+            .icon_color(Color::Placeholder)
+            .color(Color::Muted)
+            .icon_position(IconPosition::Start)
+            .tooltip(Tooltip::for_action_title(
+                "Switch Branch",
+                &zed_actions::git::Branch,
+            ))
+            .on_click(cx.listener(|_, _, window, cx| {
+                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+            }))
+            .style(ButtonStyle::Transparent);
+
+        let changes_count = self.git_panel.read(cx).total_staged_count();
+
+        let close_kb_hint =
+            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
+                Some(
+                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
+                        .suffix("Cancel"),
+                )
+            } else {
+                None
+            };
+
+        let fake_commit_kb =
+            ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
+
+        let commit_hint =
+            KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
+                .suffix(commit_label);
+
+        let focus_handle = self.focus_handle(cx);
+
+        // let next_suggestion_kb =
+        //     ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
+        // let next_suggestion_hint = next_suggestion_kb.map(|kb| {
+        //     KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
+        // });
+
+        // let prev_suggestion_kb =
+        //     ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
+        // let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
+        //     KeybindingHint::new(kb, cx.theme().colors().editor_background)
+        //         .suffix("Previous Suggestion")
+        // });
+
         v_flex()
-            .justify_between()
-            .relative()
-            .w_full()
-            .h_full()
-            .pt_2()
+            .id("editor-container")
             .bg(cx.theme().colors().editor_background)
-            .child(EditorElement::new(&self.commit_editor, panel_editor_style))
-            .child(self.render_footer(window, cx))
+            .flex_1()
+            .size_full()
+            .rounded(px(border_radius))
+            .overflow_hidden()
+            .border_1()
+            .border_color(cx.theme().colors().border_variant)
+            .py_2()
+            .px_3()
+            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+                window.focus(&editor_focus_handle);
+            }))
+            .child(
+                div()
+                    .size_full()
+                    .flex_1()
+                    .child(self.commit_editor_element(window, cx)),
+            )
+            .child(
+                h_flex()
+                    .group("commit_editor_footer")
+                    .flex_none()
+                    .w_full()
+                    .items_center()
+                    .justify_between()
+                    .w_full()
+                    .pt_2()
+                    .pb_0p5()
+                    .gap_1()
+                    .child(h_flex().gap_1().child(branch_selector).children(co_authors))
+                    .child(div().flex_1())
+                    .child(
+                        h_flex()
+                            .opacity(0.7)
+                            .group_hover("commit_editor_footer", |this| this.opacity(1.0))
+                            .items_center()
+                            .justify_end()
+                            .flex_none()
+                            .px_1()
+                            .gap_4()
+                            .children(close_kb_hint)
+                            // .children(next_suggestion_hint)
+                            .child(commit_hint),
+                    ),
+            )
     }
 
     pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -180,13 +429,10 @@ impl CommitModal {
             (branch, tooltip, title, co_authors)
         });
 
-        let branch_selector = Button::new("branch-selector", branch)
-            .color(Color::Muted)
-            .style(ButtonStyle::Subtle)
+        let branch_selector = panel_button(branch)
             .icon(IconName::GitBranch)
             .icon_size(IconSize::Small)
             .icon_color(Color::Muted)
-            .size(ButtonSize::Compact)
             .icon_position(IconPosition::Start)
             .tooltip(Tooltip::for_action_title(
                 "Switch Branch",
@@ -197,13 +443,28 @@ impl CommitModal {
             }))
             .style(ButtonStyle::Transparent);
 
+        let changes_count = self.git_panel.read(cx).total_staged_count();
+
+        let close_kb_hint =
+            if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
+                Some(
+                    KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
+                        .suffix("Cancel"),
+                )
+            } else {
+                None
+            };
+
         h_flex()
+            .items_center()
+            .h(px(36.0))
             .w_full()
             .justify_between()
-            .child(branch_selector)
+            .px_3()
+            .child(h_flex().child(branch_selector))
             .child(
-                h_flex().children(co_authors).child(
-                    panel_filled_button(title)
+                h_flex().gap_1p5().children(co_authors).child(
+                    Button::new("stage-button", title)
                         .tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
                         .on_click(cx.listener(|this, _, window, cx| {
                             this.commit(&Default::default(), window, cx);
@@ -212,6 +473,10 @@ impl CommitModal {
             )
     }
 
+    fn border_radius(&self) -> f32 {
+        8.0
+    }
+
     fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
         cx.emit(DismissEvent);
     }
@@ -224,27 +489,33 @@ impl CommitModal {
 
 impl Render for CommitModal {
     fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+        let (width, _, border_radius) = self.container_properties(window, cx);
+
         v_flex()
             .id("commit-modal")
             .key_context("GitCommit")
             .elevation_3(cx)
+            .overflow_hidden()
             .on_action(cx.listener(Self::dismiss))
             .on_action(cx.listener(Self::commit))
+            // .on_action(cx.listener(Self::next_suggestion))
+            // .on_action(cx.listener(Self::prev_suggestion))
             .relative()
-            .bg(cx.theme().colors().editor_background)
-            .rounded(px(16.))
+            .justify_between()
+            .bg(cx.theme().colors().elevated_surface_background)
+            .rounded(px(border_radius))
             .border_1()
             .border_color(cx.theme().colors().border)
-            .py_2()
-            .px_4()
-            .w(px(480.))
-            .min_h(rems(18.))
+            .w(px(width))
+            .h(px(360.))
             .flex_1()
             .overflow_hidden()
             .child(
                 v_flex()
                     .flex_1()
+                    .p_2()
                     .child(self.render_commit_editor(None, window, cx)),
             )
+        // .child(self.render_footer(window, cx))
     }
 }

crates/git_ui/src/git_panel.rs 🔗

@@ -184,6 +184,7 @@ pub struct GitPanel {
     pending_remote_operations: RemoteOperations,
     pub(crate) active_repository: Option<Entity<Repository>>,
     commit_editor: Entity<Editor>,
+    suggested_commit_message: Option<String>,
     conflicted_count: usize,
     conflicted_staged_count: usize,
     current_modifiers: Modifiers,
@@ -308,6 +309,7 @@ impl GitPanel {
                 remote_operation_id: 0,
                 active_repository,
                 commit_editor,
+                suggested_commit_message: None,
                 conflicted_count: 0,
                 conflicted_staged_count: 0,
                 current_modifiers: window.modifiers(),
@@ -1038,6 +1040,10 @@ impl GitPanel {
         .detach();
     }
 
+    pub fn total_staged_count(&self) -> usize {
+        self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
+    }
+
     pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
         self.commit_editor
             .read(cx)
@@ -1200,6 +1206,57 @@ impl GitPanel {
         self.pending_commit = Some(task);
     }
 
+    /// Suggests a commit message based on the changed files and their statuses
+    pub fn suggest_commit_message(&self) -> Option<String> {
+        let entries = self
+            .entries
+            .iter()
+            .filter_map(|entry| {
+                if let GitListEntry::GitStatusEntry(status_entry) = entry {
+                    Some(status_entry)
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<&GitStatusEntry>>();
+
+        if entries.is_empty() {
+            None
+        } else if entries.len() == 1 {
+            let entry = &entries[0];
+            let file_name = entry
+                .repo_path
+                .file_name()
+                .unwrap_or_default()
+                .to_string_lossy();
+
+            if entry.status.is_deleted() {
+                Some(format!("Delete {}", file_name))
+            } else if entry.status.is_created() {
+                Some(format!("Create {}", file_name))
+            } else if entry.status.is_modified() {
+                Some(format!("Update {}", file_name))
+            } else {
+                None
+            }
+        } else {
+            None
+        }
+    }
+
+    fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
+        let suggested_commit_message = self.suggest_commit_message();
+        self.suggested_commit_message = suggested_commit_message.clone();
+
+        if let Some(suggested_commit_message) = suggested_commit_message {
+            self.commit_editor.update(cx, |editor, cx| {
+                editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
+            });
+        }
+
+        cx.notify();
+    }
+
     fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
         let Some(repo) = self.active_repository.clone() else {
             return;
@@ -1444,6 +1501,7 @@ impl GitPanel {
                             git_panel.clear_pending();
                         }
                         git_panel.update_visible_entries(cx);
+                        git_panel.update_editor_placeholder(cx);
                     })
                     .ok();
             }

crates/gpui/src/color.rs 🔗

@@ -486,7 +486,31 @@ impl Hsla {
         self.a *= 1.0 - factor.clamp(0., 1.);
     }
 
-    /// Returns a new HSLA color with the same hue, saturation, and lightness, but with a modified alpha value.
+    /// Multiplies the alpha value of the color by a given factor
+    /// and returns a new HSLA color.
+    ///
+    /// Useful for transforming colors with dynamic opacity,
+    /// like a color from an external source.
+    ///
+    /// Example:
+    /// ```
+    /// let color = gpui::red();
+    /// let faded_color = color.opacity(0.5);
+    /// assert_eq!(faded_color.a, 0.5);
+    /// ```
+    ///
+    /// This will return a red color with half the opacity.
+    ///
+    /// Example:
+    /// ```
+    /// let color = hlsa(0.7, 1.0, 0.5, 0.7); // A saturated blue
+    /// let faded_color = color.opacity(0.16);
+    /// assert_eq!(faded_color.a, 0.112);
+    /// ```
+    ///
+    /// This will return a blue color with around ~10% opacity,
+    /// suitable for an element's hover or selected state.
+    ///
     pub fn opacity(&self, factor: f32) -> Self {
         Hsla {
             h: self.h,
@@ -495,6 +519,35 @@ impl Hsla {
             a: self.a * factor.clamp(0., 1.),
         }
     }
+
+    /// Returns a new HSLA color with the same hue, saturation,
+    /// and lightness, but with a new alpha value.
+    ///
+    /// Example:
+    /// ```
+    /// let color = gpui::red();
+    /// let red_color = color.alpha(0.25);
+    /// assert_eq!(red_color.a, 0.25);
+    /// ```
+    ///
+    /// This will return a red color with half the opacity.
+    ///
+    /// Example:
+    /// ```
+    /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
+    /// let faded_color = color.alpha(0.25);
+    /// assert_eq!(faded_color.a, 0.25);
+    /// ```
+    ///
+    /// This will return a blue color with 25% opacity.
+    pub fn alpha(&self, a: f32) -> Self {
+        Hsla {
+            h: self.h,
+            s: self.s,
+            l: self.l,
+            a: a.clamp(0., 1.),
+        }
+    }
 }
 
 impl From<Rgba> for Hsla {

crates/outline/src/outline.rs 🔗

@@ -15,7 +15,7 @@ use language::{Outline, OutlineItem};
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
 use settings::Settings;
-use theme::{color_alpha, ActiveTheme, ThemeSettings};
+use theme::{ActiveTheme, ThemeSettings};
 use ui::{prelude::*, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{DismissDecision, ModalView};
@@ -332,7 +332,7 @@ pub fn render_item<T>(
     cx: &App,
 ) -> StyledText {
     let highlight_style = HighlightStyle {
-        background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)),
+        background_color: Some(cx.theme().colors().text_accent.alpha(0.3)),
         ..Default::default()
     };
     let custom_highlights = match_ranges

crates/panel/src/panel.rs 🔗

@@ -49,6 +49,7 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
     let id = ElementId::Name(label.clone().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/theme/src/theme.rs 🔗

@@ -330,14 +330,6 @@ impl Theme {
     }
 }
 
-/// Compounds a color with an alpha value.
-/// TODO: Replace this with a method on Hsla.
-pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla {
-    let mut color = color;
-    color.a = alpha;
-    color
-}
-
 /// Asynchronously reads the user theme from the specified path.
 pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
     let reader = fs.open_sync(theme_path).await?;

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

@@ -1,7 +1,8 @@
+use crate::KeyBinding;
 use crate::{h_flex, prelude::*};
-use crate::{ElevationIndex, KeyBinding};
-use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
+use gpui::{point, AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window};
 use smallvec::smallvec;
+use theme::Appearance;
 
 /// Represents a hint for a keybinding, optionally with a prefix and suffix.
 ///
@@ -23,7 +24,7 @@ pub struct KeybindingHint {
     suffix: Option<SharedString>,
     keybinding: KeyBinding,
     size: Option<Pixels>,
-    elevation: Option<ElevationIndex>,
+    background_color: Hsla,
 }
 
 impl KeybindingHint {
@@ -37,15 +38,15 @@ impl KeybindingHint {
     /// ```
     /// use ui::prelude::*;
     ///
-    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"));
+    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
     /// ```
-    pub fn new(keybinding: KeyBinding) -> Self {
+    pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self {
         Self {
             prefix: None,
             suffix: None,
             keybinding,
             size: None,
-            elevation: None,
+            background_color,
         }
     }
 
@@ -59,15 +60,19 @@ impl KeybindingHint {
     /// ```
     /// use ui::prelude::*;
     ///
-    /// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"));
+    /// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
     /// ```
-    pub fn with_prefix(prefix: impl Into<SharedString>, keybinding: KeyBinding) -> Self {
+    pub fn with_prefix(
+        prefix: impl Into<SharedString>,
+        keybinding: KeyBinding,
+        background_color: Hsla,
+    ) -> Self {
         Self {
             prefix: Some(prefix.into()),
             suffix: None,
             keybinding,
             size: None,
-            elevation: None,
+            background_color,
         }
     }
 
@@ -81,15 +86,19 @@ impl KeybindingHint {
     /// ```
     /// use ui::prelude::*;
     ///
-    /// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste");
+    /// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste", Hsla::new(0.0, 0.0, 0.0, 1.0));
     /// ```
-    pub fn with_suffix(keybinding: KeyBinding, suffix: impl Into<SharedString>) -> Self {
+    pub fn with_suffix(
+        keybinding: KeyBinding,
+        suffix: impl Into<SharedString>,
+        background_color: Hsla,
+    ) -> Self {
         Self {
             prefix: None,
             suffix: Some(suffix.into()),
             keybinding,
             size: None,
-            elevation: None,
+            background_color,
         }
     }
 
@@ -143,46 +152,37 @@ impl KeybindingHint {
         self.size = size.into();
         self
     }
-
-    /// Sets the elevation of the keybinding hint.
-    ///
-    /// This method allows specifying the elevation index for the keybinding hint,
-    /// which affects its visual appearance in terms of depth or layering.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// use ui::prelude::*;
-    ///
-    /// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+A"))
-    ///     .elevation(ElevationIndex::new(1));
-    /// ```
-    pub fn elevation(mut self, elevation: impl Into<Option<ElevationIndex>>) -> Self {
-        self.elevation = elevation.into();
-        self
-    }
 }
 
 impl RenderOnce for KeybindingHint {
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let colors = cx.theme().colors().clone();
+        let is_light = cx.theme().appearance() == Appearance::Light;
+
+        let border_color =
+            self.background_color
+                .blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
+        let bg_color =
+            self.background_color
+                .blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
+        let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
 
         let size = self
             .size
             .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
         let kb_size = size - px(2.0);
-        let kb_bg = if let Some(elevation) = self.elevation {
-            elevation.on_elevation_bg(cx)
-        } else {
-            theme::color_alpha(colors.element_background, 0.6)
-        };
 
-        h_flex()
-            .items_center()
+        let mut base = h_flex();
+
+        base.text_style()
+            .get_or_insert_with(Default::default)
+            .font_style = Some(FontStyle::Italic);
+
+        base.items_center()
             .gap_0p5()
             .font_buffer(cx)
             .text_size(size)
-            .text_color(colors.text_muted)
+            .text_color(colors.text_disabled)
             .children(self.prefix)
             .child(
                 h_flex()
@@ -191,10 +191,10 @@ impl RenderOnce for KeybindingHint {
                     .px_0p5()
                     .mr_0p5()
                     .border_1()
-                    .border_color(kb_bg)
-                    .bg(kb_bg.opacity(0.8))
+                    .border_color(border_color)
+                    .bg(bg_color)
                     .shadow(smallvec![BoxShadow {
-                        color: cx.theme().colors().editor_background.opacity(0.8),
+                        color: shadow_color,
                         offset: point(px(0.), px(1.)),
                         blur_radius: px(0.),
                         spread_radius: px(0.),
@@ -212,6 +212,8 @@ impl ComponentPreview for KeybindingHint {
         let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
             .unwrap_or(KeyBinding::new(enter_fallback, cx));
 
+        let bg_color = cx.theme().colors().surface_background;
+
         v_flex()
             .gap_6()
             .children(vec![
@@ -220,17 +222,17 @@ impl ComponentPreview for KeybindingHint {
                     vec![
                         single_example(
                             "With Prefix",
-                            KeybindingHint::with_prefix("Go to Start:", enter.clone())
+                            KeybindingHint::with_prefix("Go to Start:", enter.clone(), bg_color)
                                 .into_any_element(),
                         ),
                         single_example(
                             "With Suffix",
-                            KeybindingHint::with_suffix(enter.clone(), "Go to End")
+                            KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
                                 .into_any_element(),
                         ),
                         single_example(
                             "With Prefix and Suffix",
-                            KeybindingHint::new(enter.clone())
+                            KeybindingHint::new(enter.clone(), bg_color)
                                 .prefix("Confirm:")
                                 .suffix("Execute selected action")
                                 .into_any_element(),
@@ -242,21 +244,21 @@ impl ComponentPreview for KeybindingHint {
                     vec![
                         single_example(
                             "Small",
-                            KeybindingHint::new(enter.clone())
+                            KeybindingHint::new(enter.clone(), bg_color)
                                 .size(Pixels::from(12.0))
                                 .prefix("Small:")
                                 .into_any_element(),
                         ),
                         single_example(
                             "Medium",
-                            KeybindingHint::new(enter.clone())
+                            KeybindingHint::new(enter.clone(), bg_color)
                                 .size(Pixels::from(16.0))
                                 .suffix("Medium")
                                 .into_any_element(),
                         ),
                         single_example(
                             "Large",
-                            KeybindingHint::new(enter.clone())
+                            KeybindingHint::new(enter.clone(), bg_color)
                                 .size(Pixels::from(20.0))
                                 .prefix("Large:")
                                 .suffix("Size")
@@ -264,41 +266,6 @@ impl ComponentPreview for KeybindingHint {
                         ),
                     ],
                 ),
-                example_group_with_title(
-                    "Elevations",
-                    vec![
-                        single_example(
-                            "Surface",
-                            KeybindingHint::new(enter.clone())
-                                .elevation(ElevationIndex::Surface)
-                                .prefix("Surface:")
-                                .into_any_element(),
-                        ),
-                        single_example(
-                            "Elevated Surface",
-                            KeybindingHint::new(enter.clone())
-                                .elevation(ElevationIndex::ElevatedSurface)
-                                .suffix("Elevated")
-                                .into_any_element(),
-                        ),
-                        single_example(
-                            "Editor Surface",
-                            KeybindingHint::new(enter.clone())
-                                .elevation(ElevationIndex::EditorSurface)
-                                .prefix("Editor:")
-                                .suffix("Surface")
-                                .into_any_element(),
-                        ),
-                        single_example(
-                            "Modal Surface",
-                            KeybindingHint::new(enter.clone())
-                                .elevation(ElevationIndex::ModalSurface)
-                                .prefix("Modal:")
-                                .suffix("Enter")
-                                .into_any_element(),
-                        ),
-                    ],
-                ),
             ])
             .into_any_element()
     }

crates/ui/src/styles/elevation.rs 🔗

@@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter};
 
 use gpui::{hsla, point, px, App, BoxShadow, Hsla};
 use smallvec::{smallvec, SmallVec};
-use theme::ActiveTheme;
+use theme::{ActiveTheme, Appearance};
 
 /// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
 ///
@@ -40,27 +40,37 @@ impl Display for ElevationIndex {
 
 impl ElevationIndex {
     /// Returns an appropriate shadow for the given elevation index.
-    pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> {
+    pub fn shadow(self, cx: &App) -> SmallVec<[BoxShadow; 2]> {
+        let is_light = cx.theme().appearance() == Appearance::Light;
+
         match self {
             ElevationIndex::Surface => smallvec![],
             ElevationIndex::EditorSurface => smallvec![],
 
-            ElevationIndex::ElevatedSurface => smallvec![BoxShadow {
-                color: hsla(0., 0., 0., 0.12),
-                offset: point(px(0.), px(2.)),
-                blur_radius: px(3.),
-                spread_radius: px(0.),
-            }],
+            ElevationIndex::ElevatedSurface => smallvec![
+                BoxShadow {
+                    color: hsla(0., 0., 0., 0.12),
+                    offset: point(px(0.), px(2.)),
+                    blur_radius: px(3.),
+                    spread_radius: px(0.),
+                },
+                BoxShadow {
+                    color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }),
+                    offset: point(px(1.), px(1.)),
+                    blur_radius: px(0.),
+                    spread_radius: px(0.),
+                }
+            ],
 
             ElevationIndex::ModalSurface => smallvec![
                 BoxShadow {
-                    color: hsla(0., 0., 0., 0.12),
+                    color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }),
                     offset: point(px(0.), px(2.)),
                     blur_radius: px(3.),
                     spread_radius: px(0.),
                 },
                 BoxShadow {
-                    color: hsla(0., 0., 0., 0.08),
+                    color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }),
                     offset: point(px(0.), px(3.)),
                     blur_radius: px(6.),
                     spread_radius: px(0.),
@@ -71,6 +81,12 @@ impl ElevationIndex {
                     blur_radius: px(12.),
                     spread_radius: px(0.),
                 },
+                BoxShadow {
+                    color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }),
+                    offset: point(px(1.), px(1.)),
+                    blur_radius: px(0.),
+                    spread_radius: px(0.),
+                },
             ],
 
             _ => smallvec![],

crates/ui/src/traits/styled_ext.rs 🔗

@@ -8,13 +8,13 @@ fn elevated<E: Styled>(this: E, cx: &App, index: ElevationIndex) -> E {
         .rounded_lg()
         .border_1()
         .border_color(cx.theme().colors().border_variant)
-        .shadow(index.shadow())
+        .shadow(index.shadow(cx))
 }
 
 fn elevated_borderless<E: Styled>(this: E, cx: &mut App, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
         .rounded_lg()
-        .shadow(index.shadow())
+        .shadow(index.shadow(cx))
 }
 
 /// Extends [`gpui::Styled`] with Zed-specific styling methods.