From 30af8d0a8153697b653adf1cd5e3c9b5f283bf27 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 24 Feb 2025 14:19:06 -0500 Subject: [PATCH] git_ui: Commit modal refinement (#25484) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Mikayla Maki --- assets/keymaps/default-linux.json | 8 + assets/keymaps/default-macos.json | 19 +- .../src/context_editor.rs | 4 +- .../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 | 38 ++- crates/ui/src/traits/styled_ext.rs | 4 +- 14 files changed, 510 insertions(+), 146 deletions(-) delete mode 100644 crates/git_ui/src/quick_commit.rs diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 7221a16172..4fc002c1d9 100644 --- a/assets/keymaps/default-linux.json +++ b/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": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a3f60175f2..ceb84b4c65 100644 --- a/assets/keymaps/default-macos.json +++ b/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" } }, { @@ -742,14 +743,6 @@ "escape": "git_panel::ToggleFocus" } }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::Newline", - "cmd-enter": "git::Commit" - } - }, { "context": "GitPanel > Editor", "use_key_equivalents": true, @@ -761,6 +754,14 @@ "alt-up": "git_panel::FocusChanges" } }, + { + "context": "GitCommit > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "editor::Newline", + "cmd-enter": "git::Commit" + } + }, { "context": "CollabPanel && not_editing", "use_key_equivalents": true, diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index e3be2eec77..578d4636c9 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/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") diff --git a/crates/extensions_ui/src/components/extension_card.rs b/crates/extensions_ui/src/components/extension_card.rs index 901e8db075..d9ae378011 100644 --- a/crates/extensions_ui/src/components/extension_card.rs +++ b/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.")), ) }), diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 2ead1cb37b..8decb4f82d 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/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, commit_editor: Entity, restore_dock: RestoreDock, + current_suggestion: Option, + suggested_messages: Vec, } impl Focusable for CommitModal { @@ -114,6 +122,7 @@ impl CommitModal { cx: &mut Context, ) -> 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) -> (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) { + // 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.current_suggestion = Some(1); + // self.apply_suggestion(window, cx); + // } + + // fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context) { + // 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.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) { + // 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) -> 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, ) -> impl IntoElement { - let editor = self.commit_editor.clone(); + let (width, padding_x, modal_border_radius) = self.container_properties(window, cx); - let panel_editor_style = panel_editor_style(true, window, cx); + let border_radius = modal_border_radius - padding_x / 2.0; + + 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(|| "".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) -> 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) { 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)) } } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index eeee040c88..46a5ba059b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -184,6 +184,7 @@ pub struct GitPanel { pending_remote_operations: RemoteOperations, pub(crate) active_repository: Option>, commit_editor: Entity, + suggested_commit_message: Option, 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 { 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 { + let entries = self + .entries + .iter() + .filter_map(|entry| { + if let GitListEntry::GitStatusEntry(status_entry) = entry { + Some(status_entry) + } else { + None + } + }) + .collect::>(); + + 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) { + 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) { 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(); } diff --git a/crates/git_ui/src/quick_commit.rs b/crates/git_ui/src/quick_commit.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index daec9440a4..149d924b6f 100644 --- a/crates/gpui/src/color.rs +++ b/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 for Hsla { diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 115aa97519..3d119f9b88 100644 --- a/crates/outline/src/outline.rs +++ b/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( 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 diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 934d8281a3..59572c402b 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -49,6 +49,7 @@ pub fn panel_button(label: impl Into) -> 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) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a046961c1e..1b9bc1033e 100644 --- a/crates/theme/src/theme.rs +++ b/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) -> Result { let reader = fs.open_sync(theme_path).await?; diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 9df64be5f6..7b9d553848 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/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, keybinding: KeyBinding, size: Option, - elevation: Option, + 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, keybinding: KeyBinding) -> Self { + pub fn with_prefix( + prefix: impl Into, + 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) -> Self { + pub fn with_suffix( + keybinding: KeyBinding, + suffix: impl Into, + 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>) -> 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() } diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index f12a16e91d..aea91c8d5f 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/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,19 +40,14 @@ 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::ModalSurface => smallvec![ + ElevationIndex::ElevatedSurface => smallvec![ BoxShadow { color: hsla(0., 0., 0., 0.12), offset: point(px(0.), px(2.)), @@ -60,7 +55,22 @@ impl ElevationIndex { spread_radius: px(0.), }, BoxShadow { - color: hsla(0., 0., 0., 0.08), + 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., 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., 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![], diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs index 48a515afd7..76da92d004 100644 --- a/crates/ui/src/traits/styled_ext.rs +++ b/crates/ui/src/traits/styled_ext.rs @@ -8,13 +8,13 @@ fn elevated(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(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.