git_ui: Update git panel commit editor, start on quick commit

Nate Butler created

- Fixes commit editor issues & updates style
- Starts on quick commit (not hooked up to anything)
- Updates some panel styles
- Adds SwitchWithLabel
- 
Release Notes:

- N/A

Change summary

Cargo.lock                                     |   3 
crates/git_ui/src/git_panel.rs                 | 239 ++++++++-------
crates/git_ui/src/git_ui.rs                    |   2 
crates/git_ui/src/quick_commit.rs              | 307 ++++++++++++++++++++
crates/panel/Cargo.toml                        |   3 
crates/panel/src/panel.rs                      |  65 ++++
crates/ui/src/components/button/button.rs      |   8 
crates/ui/src/components/button/button_like.rs |   4 
crates/ui/src/components/button/icon_button.rs |   9 
crates/ui/src/components/toggle.rs             |  58 +++
10 files changed, 582 insertions(+), 116 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9044,7 +9044,10 @@ dependencies = [
 name = "panel"
 version = "0.1.0"
 dependencies = [
+ "editor",
  "gpui",
+ "settings",
+ "theme",
  "ui",
  "workspace",
 ]

crates/git_ui/src/git_panel.rs 🔗

@@ -6,33 +6,32 @@ use crate::{
 };
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
-use editor::actions::MoveToEnd;
-use editor::scroll::ScrollbarAutoHide;
-use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
-use git::repository::RepoPath;
-use git::status::FileStatus;
-use git::{Commit, ToggleStaged};
+use editor::{
+    actions::MoveToEnd, scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode,
+    EditorSettings, MultiBuffer, ShowScrollbar,
+};
+use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
 use gpui::*;
 use language::{Buffer, File};
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
 use multi_buffer::ExcerptInfo;
-use panel::PanelHeader;
-use project::git::{GitEvent, Repository};
-use project::{Fs, Project, ProjectPath};
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader};
+use project::{
+    git::{GitEvent, Repository},
+    Fs, Project, ProjectPath,
+};
 use serde::{Deserialize, Serialize};
 use settings::Settings as _;
 use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
-use theme::ThemeSettings;
 use ui::{
-    prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors,
-    ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
+    prelude::*, ButtonLike, Checkbox, CheckboxWithLabel, Divider, DividerColor, ElevationIndex,
+    IndentGuideColors, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
-use workspace::notifications::{DetachAndPromptErr, NotificationId};
-use workspace::Toast;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
+    notifications::{DetachAndPromptErr, NotificationId},
+    Toast, Workspace,
 };
 
 actions!(
@@ -147,33 +146,33 @@ struct PendingOperation {
 }
 
 pub struct GitPanel {
+    active_repository: Option<Entity<Repository>>,
+    commit_editor: Entity<Editor>,
+    conflicted_count: usize,
+    conflicted_staged_count: usize,
     current_modifiers: Modifiers,
+    enable_auto_coauthors: bool,
+    entries: Vec<GitListEntry>,
+    entries_by_path: collections::HashMap<RepoPath, usize>,
     focus_handle: FocusHandle,
     fs: Arc<dyn Fs>,
     hide_scrollbar_task: Option<Task<()>>,
+    new_count: usize,
+    new_staged_count: usize,
+    pending: Vec<PendingOperation>,
+    pending_commit: Option<Task<()>>,
     pending_serialization: Task<Option<()>>,
-    workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
-    active_repository: Option<Entity<Repository>>,
+    repository_selector: Entity<RepositorySelector>,
     scroll_handle: UniformListScrollHandle,
     scrollbar_state: ScrollbarState,
     selected_entry: Option<usize>,
     show_scrollbar: bool,
+    tracked_count: usize,
+    tracked_staged_count: usize,
     update_visible_entries_task: Task<()>,
-    repository_selector: Entity<RepositorySelector>,
-    commit_editor: Entity<Editor>,
-    entries: Vec<GitListEntry>,
-    entries_by_path: collections::HashMap<RepoPath, usize>,
     width: Option<Pixels>,
-    pending: Vec<PendingOperation>,
-    pending_commit: Option<Task<()>>,
-
-    conflicted_staged_count: usize,
-    conflicted_count: usize,
-    tracked_staged_count: usize,
-    tracked_count: usize,
-    new_staged_count: usize,
-    new_count: usize,
+    workspace: WeakEntity<Workspace>,
 }
 
 fn commit_message_editor(
@@ -181,23 +180,10 @@ fn commit_message_editor(
     window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) -> Editor {
-    let theme = ThemeSettings::get_global(cx);
-
-    let mut text_style = window.text_style();
-    let refinement = TextStyleRefinement {
-        font_family: Some(theme.buffer_font.family.clone()),
-        font_features: Some(FontFeatures::disable_ligatures()),
-        font_size: Some(px(12.).into()),
-        color: Some(cx.theme().colors().editor_foreground),
-        background_color: Some(gpui::transparent_black()),
-        ..Default::default()
-    };
-    text_style.refine(&refinement);
-
     let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
         let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
         Editor::new(
-            EditorMode::AutoHeight { max_lines: 10 },
+            EditorMode::AutoHeight { max_lines: 6 },
             buffer,
             None,
             false,
@@ -205,13 +191,12 @@ fn commit_message_editor(
             cx,
         )
     } else {
-        Editor::auto_height(10, window, cx)
+        Editor::auto_height(6, window, cx)
     };
     commit_editor.set_use_autoclose(false);
     commit_editor.set_show_gutter(false, cx);
     commit_editor.set_show_wrap_guides(false, cx);
     commit_editor.set_show_indent_guides(false, cx);
-    commit_editor.set_text_style_refinement(refinement);
     commit_editor.set_placeholder_text("Enter commit message", cx);
     commit_editor
 }
@@ -260,37 +245,40 @@ impl GitPanel {
             )
             .detach();
 
+            let scrollbar_state =
+                ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
+
             let repository_selector =
                 cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
 
             let mut git_panel = Self {
-                focus_handle: cx.focus_handle(),
-                pending_serialization: Task::ready(None),
+                active_repository,
+                commit_editor,
+                conflicted_count: 0,
+                conflicted_staged_count: 0,
+                current_modifiers: window.modifiers(),
+                enable_auto_coauthors: true,
                 entries: Vec::new(),
                 entries_by_path: HashMap::default(),
+                focus_handle: cx.focus_handle(),
+                fs,
+                hide_scrollbar_task: None,
+                new_count: 0,
+                new_staged_count: 0,
                 pending: Vec::new(),
-                current_modifiers: window.modifiers(),
-                width: Some(px(360.)),
-                scrollbar_state: ScrollbarState::new(scroll_handle.clone())
-                    .parent_entity(&cx.entity()),
+                pending_commit: None,
+                pending_serialization: Task::ready(None),
+                project,
                 repository_selector,
+                scroll_handle,
+                scrollbar_state,
                 selected_entry: None,
                 show_scrollbar: false,
-                hide_scrollbar_task: None,
+                tracked_count: 0,
+                tracked_staged_count: 0,
                 update_visible_entries_task: Task::ready(()),
-                pending_commit: None,
-                active_repository,
-                scroll_handle,
-                fs,
-                commit_editor,
-                project,
+                width: Some(px(360.)),
                 workspace,
-                conflicted_count: 0,
-                conflicted_staged_count: 0,
-                tracked_staged_count: 0,
-                tracked_count: 0,
-                new_staged_count: 0,
-                new_count: 0,
             };
             git_panel.schedule_update(false, window, cx);
             git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -990,6 +978,26 @@ impl GitPanel {
         cx.notify();
     }
 
+    fn toggle_auto_coauthors(&mut self, cx: &mut Context<Self>) {
+        self.enable_auto_coauthors = !self.enable_auto_coauthors;
+        cx.notify();
+    }
+
+    fn header_state(&self, header_type: Section) -> ToggleState {
+        let (staged_count, count) = match header_type {
+            Section::New => (self.new_staged_count, self.new_count),
+            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
+            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
+        };
+        if staged_count == 0 {
+            ToggleState::Unselected
+        } else if count == staged_count {
+            ToggleState::Selected
+        } else {
+            ToggleState::Indeterminate
+        }
+    }
+
     fn update_counts(&mut self, repo: &Repository) {
         self.conflicted_count = 0;
         self.conflicted_staged_count = 0;
@@ -1043,21 +1051,6 @@ impl GitPanel {
         self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
     }
 
-    fn header_state(&self, header_type: Section) -> ToggleState {
-        let (staged_count, count) = match header_type {
-            Section::New => (self.new_staged_count, self.new_count),
-            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
-            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
-        };
-        if staged_count == 0 {
-            ToggleState::Unselected
-        } else if count == staged_count {
-            ToggleState::Selected
-        } else {
-            ToggleState::Indeterminate
-        }
-    }
-
     fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
@@ -1165,13 +1158,21 @@ impl GitPanel {
         )
     }
 
-    pub fn render_commit_editor(&self, cx: &Context<Self>) -> impl IntoElement {
+    pub fn render_commit_editor(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         let editor = self.commit_editor.clone();
         let can_commit = (self.has_staged_changes() || self.has_tracked_changes())
             && self.pending_commit.is_none()
             && !editor.read(cx).is_empty(cx)
             && !self.has_unstaged_conflicts()
             && self.has_write_access(cx);
+        // let can_commit_all =
+        //     !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
+        let panel_editor_style = panel_editor_style(true, window, cx);
+
         let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
 
         let focus_handle_1 = self.focus_handle(cx).clone();
@@ -1186,8 +1187,7 @@ impl GitPanel {
             "Commit All"
         };
 
-        let commit_button = self
-            .panel_button("commit-changes", title)
+        let commit_button = panel_filled_button(title)
             .tooltip(move |window, cx| {
                 let focus_handle = focus_handle_1.clone();
                 Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
@@ -1197,28 +1197,50 @@ impl GitPanel {
                 cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx))
             });
 
-        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
-            v_flex()
-                .id("commit-editor-container")
-                .relative()
-                .h_full()
-                .py_2p5()
-                .px_3()
-                .bg(cx.theme().colors().editor_background)
-                .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
-                    window.focus(&editor_focus_handle);
-                }))
-                .child(self.commit_editor.clone())
-                .child(
-                    h_flex()
-                        .absolute()
-                        .bottom_2p5()
-                        .right_3()
-                        .gap_1p5()
-                        .child(div().gap_1().flex_grow())
-                        .child(commit_button),
-                ),
-        )
+        let enable_coauthors = CheckboxWithLabel::new(
+            "enable-coauthors",
+            Label::new("Add Co-authors")
+                .color(Color::Disabled)
+                .size(LabelSize::XSmall),
+            self.enable_auto_coauthors.into(),
+            cx.listener(move |this, _, _, cx| this.toggle_auto_coauthors(cx)),
+        );
+
+        let footer_size = px(32.);
+        let gap = px(16.0);
+
+        let max_height = window.line_height() * 6. + gap + footer_size;
+
+        panel_editor_container(window, cx)
+            .id("commit-editor-container")
+            .relative()
+            .h(max_height)
+            .w_full()
+            .border_t_1()
+            .border_color(cx.theme().colors().border)
+            .bg(cx.theme().colors().editor_background)
+            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+                window.focus(&editor_focus_handle);
+            }))
+            .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+            .child(
+                h_flex()
+                    .absolute()
+                    .bottom_0()
+                    .left_2()
+                    .h(footer_size)
+                    .flex_none()
+                    .child(enable_coauthors),
+            )
+            .child(
+                h_flex()
+                    .absolute()
+                    .bottom_0()
+                    .right_2()
+                    .h(footer_size)
+                    .flex_none()
+                    .child(commit_button),
+            )
     }
 
     fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1348,6 +1370,7 @@ impl GitPanel {
 
         v_flex()
             .size_full()
+            .flex_grow()
             .overflow_hidden()
             .child(
                 uniform_list(cx.entity().clone(), "entries", entry_count, {
@@ -1496,7 +1519,7 @@ impl GitPanel {
                     .spacing(ListItemSpacing::Sparse)
                     .start_slot(start_slot)
                     .toggle_state(selected)
-                    .focused(selected && self.focus_handle.is_focused(window))
+                    .focused(selected && self.focus_handle(cx).is_focused(window))
                     .disabled(!has_write_access)
                     .on_click({
                         cx.listener(move |this, _, _, cx| {
@@ -1599,7 +1622,7 @@ impl GitPanel {
                     .spacing(ListItemSpacing::Sparse)
                     .start_slot(start_slot)
                     .toggle_state(selected)
-                    .focused(selected && self.focus_handle.is_focused(window))
+                    .focused(selected && self.focus_handle(cx).is_focused(window))
                     .disabled(!has_write_access)
                     .on_click({
                         cx.listener(move |this, _, window, cx| {
@@ -1705,7 +1728,7 @@ impl Render for GitPanel {
             } else {
                 self.render_empty_state(cx).into_any_element()
             })
-            .child(self.render_commit_editor(cx))
+            .child(self.render_commit_editor(window, cx))
     }
 }
 

crates/git_ui/src/git_ui.rs 🔗

@@ -9,12 +9,14 @@ pub mod branch_picker;
 pub mod git_panel;
 mod git_panel_settings;
 pub mod project_diff;
+// mod quick_commit;
 pub mod repository_selector;
 
 pub fn init(cx: &mut App) {
     GitPanelSettings::register(cx);
     branch_picker::init(cx);
     cx.observe_new(ProjectDiff::register).detach();
+    // quick_commit::init(cx);
 }
 
 // TODO: Add updated status colors to theme

crates/git_ui/src/quick_commit.rs 🔗

@@ -0,0 +1,307 @@
+#![allow(unused, dead_code)]
+
+use crate::repository_selector::RepositorySelector;
+use anyhow::Result;
+use git::{CommitAllChanges, CommitChanges};
+use language::Buffer;
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
+use ui::{prelude::*, Tooltip};
+
+use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
+use gpui::*;
+use project::git::Repository;
+use project::{Fs, Project};
+use std::sync::Arc;
+use workspace::{ModalView, Workspace};
+
+actions!(
+    git,
+    [QuickCommitWithMessage, QuickCommitStaged, QuickCommitAll]
+);
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, window, cx| {
+        let Some(window) = window else {
+            return;
+        };
+        QuickCommitModal::register(workspace, window, cx)
+    })
+    .detach();
+}
+
+fn commit_message_editor(
+    commit_message_buffer: Option<Entity<Buffer>>,
+    window: &mut Window,
+    cx: &mut Context<'_, Editor>,
+) -> Editor {
+    let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
+        let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
+        Editor::new(
+            EditorMode::AutoHeight { max_lines: 10 },
+            buffer,
+            None,
+            false,
+            window,
+            cx,
+        )
+    } else {
+        Editor::auto_height(10, window, cx)
+    };
+    commit_editor.set_use_autoclose(false);
+    commit_editor.set_show_gutter(false, cx);
+    commit_editor.set_show_wrap_guides(false, cx);
+    commit_editor.set_show_indent_guides(false, cx);
+    commit_editor.set_placeholder_text("Enter commit message", cx);
+    commit_editor
+}
+
+pub struct QuickCommitModal {
+    focus_handle: FocusHandle,
+    fs: Arc<dyn Fs>,
+    project: Entity<Project>,
+    active_repository: Option<Entity<Repository>>,
+    repository_selector: Entity<RepositorySelector>,
+    commit_editor: Entity<Editor>,
+    width: Option<Pixels>,
+    commit_task: Task<Result<()>>,
+    commit_pending: bool,
+    can_commit: bool,
+    can_commit_all: bool,
+    enable_auto_coauthors: bool,
+}
+
+impl Focusable for QuickCommitModal {
+    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for QuickCommitModal {}
+impl ModalView for QuickCommitModal {}
+
+impl QuickCommitModal {
+    pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
+        workspace.register_action(|workspace, _: &QuickCommitWithMessage, window, cx| {
+            let project = workspace.project().clone();
+            let fs = workspace.app_state().fs.clone();
+
+            workspace.toggle_modal(window, cx, move |window, cx| {
+                QuickCommitModal::new(project, fs, window, None, cx)
+            });
+        });
+    }
+
+    pub fn new(
+        project: Entity<Project>,
+        fs: Arc<dyn Fs>,
+        window: &mut Window,
+        commit_message_buffer: Option<Entity<Buffer>>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let git_state = project.read(cx).git_state().clone();
+        let active_repository = project.read(cx).active_repository(cx);
+
+        let focus_handle = cx.focus_handle();
+
+        let commit_editor = cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+        commit_editor.update(cx, |editor, cx| {
+            editor.clear(window, cx);
+        });
+
+        let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
+
+        Self {
+            focus_handle,
+            fs,
+            project,
+            active_repository,
+            repository_selector,
+            commit_editor,
+            width: None,
+            commit_task: Task::ready(Ok(())),
+            commit_pending: false,
+            can_commit: false,
+            can_commit_all: false,
+            enable_auto_coauthors: true,
+        }
+    }
+
+    pub fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let all_repositories = self
+            .project
+            .read(cx)
+            .git_state()
+            .read(cx)
+            .all_repositories();
+        let entry_count = self
+            .active_repository
+            .as_ref()
+            .map_or(0, |repo| repo.read(cx).entry_count());
+
+        let changes_string = match entry_count {
+            0 => "No changes".to_string(),
+            1 => "1 change".to_string(),
+            n => format!("{} changes", n),
+        };
+
+        div().absolute().top_0().right_0().child(
+            panel_icon_button("open_change_list", IconName::PanelRight)
+                .disabled(true)
+                .tooltip(Tooltip::text("Changes list coming soon!")),
+        )
+    }
+
+    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 can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
+        let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
+
+        let focus_handle_1 = self.focus_handle(cx).clone();
+        let focus_handle_2 = self.focus_handle(cx).clone();
+
+        let panel_editor_style = panel_editor_style(true, window, cx);
+
+        let commit_staged_button = panel_filled_button("Commit")
+            .tooltip(move |window, cx| {
+                let focus_handle = focus_handle_1.clone();
+                Tooltip::for_action_in(
+                    "Commit all staged changes",
+                    &CommitChanges,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            })
+            .when(!can_commit, |this| {
+                this.disabled(true).style(ButtonStyle::Transparent)
+            });
+        // .on_click({
+        //     let name_and_email = name_and_email.clone();
+        //     cx.listener(move |this, _: &ClickEvent, window, cx| {
+        //         this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx)
+        //     })
+        // });
+
+        let commit_all_button = panel_filled_button("Commit All")
+            .tooltip(move |window, cx| {
+                let focus_handle = focus_handle_2.clone();
+                Tooltip::for_action_in(
+                    "Commit all changes, including unstaged changes",
+                    &CommitAllChanges,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            })
+            .when(!can_commit, |this| {
+                this.disabled(true).style(ButtonStyle::Transparent)
+            });
+        // .on_click({
+        //     let name_and_email = name_and_email.clone();
+        //     cx.listener(move |this, _: &ClickEvent, window, cx| {
+        //         this.commit_tracked_changes(
+        //             &CommitAllChanges,
+        //             name_and_email.clone(),
+        //             window,
+        //             cx,
+        //         )
+        //     })
+        // });
+
+        let co_author_button = panel_icon_button("add-co-author", IconName::UserGroup)
+            .icon_color(if self.enable_auto_coauthors {
+                Color::Muted
+            } else {
+                Color::Accent
+            })
+            .icon_size(IconSize::Small)
+            .toggle_state(self.enable_auto_coauthors)
+            // .on_click({
+            //     cx.listener(move |this, _: &ClickEvent, _, cx| {
+            //         this.toggle_auto_coauthors(cx);
+            //     })
+            // })
+            .tooltip(move |window, cx| {
+                Tooltip::with_meta(
+                    "Toggle automatic co-authors",
+                    None,
+                    "Automatically adds current collaborators",
+                    window,
+                    cx,
+                )
+            });
+
+        panel_editor_container(window, cx)
+            .id("commit-editor-container")
+            .relative()
+            .w_full()
+            .border_t_1()
+            .border_color(cx.theme().colors().border)
+            .h(px(140.))
+            .bg(cx.theme().colors().editor_background)
+            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+                window.focus(&editor_focus_handle);
+            }))
+            .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+            .child(div().flex_1())
+            .child(
+                h_flex()
+                    .items_center()
+                    .h_8()
+                    .justify_between()
+                    .gap_1()
+                    .child(co_author_button)
+                    .child(commit_all_button)
+                    .child(commit_staged_button),
+            )
+    }
+
+    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        h_flex()
+            .w_full()
+            .justify_between()
+            .child(h_flex().child("cmd+esc clear message"))
+            .child(
+                h_flex()
+                    .child(panel_filled_button("Commit"))
+                    .child(panel_filled_button("Commit All")),
+            )
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
+}
+
+impl Render for QuickCommitModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+        v_flex()
+            .id("quick-commit-modal")
+            .key_context("QuickCommit")
+            .on_action(cx.listener(Self::dismiss))
+            .relative()
+            .bg(cx.theme().colors().elevated_surface_background)
+            .rounded(px(16.))
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .py_2()
+            .px_4()
+            .w(self.width.unwrap_or(px(640.)))
+            .h(px(450.))
+            .flex_1()
+            .overflow_hidden()
+            .child(self.render_header(window, cx))
+            .child(
+                v_flex()
+                    .flex_1()
+                    // TODO: pass name_and_email
+                    .child(self.render_commit_editor(None, window, cx)),
+            )
+            .child(self.render_footer(window, cx))
+    }
+}

crates/panel/Cargo.toml 🔗

@@ -12,6 +12,9 @@ workspace = true
 path = "src/panel.rs"
 
 [dependencies]
+editor.workspace = true
 gpui.workspace = true
+settings.workspace = true
+theme.workspace = true
 ui.workspace = true
 workspace.workspace = true

crates/panel/src/panel.rs 🔗

@@ -1,5 +1,8 @@
 //! # panel
-use gpui::actions;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{actions, Entity, TextStyle};
+use settings::Settings;
+use theme::ThemeSettings;
 use ui::{prelude::*, Tab};
 
 actions!(panel, [NextPanelTab, PreviousPanelTab]);
@@ -46,7 +49,8 @@ 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)
-        .layer(ui::ElevationIndex::Surface)
+        // TODO: Change this once we use on_surface_bg in button_like
+        .layer(ui::ElevationIndex::ModalSurface)
         .size(ui::ButtonSize::Compact)
 }
 
@@ -57,10 +61,65 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
 pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
     let id = ElementId::Name(id.into());
     ui::IconButton::new(id, icon)
-        .layer(ui::ElevationIndex::Surface)
+        // TODO: Change this once we use on_surface_bg in button_like
+        .layer(ui::ElevationIndex::ModalSurface)
         .size(ui::ButtonSize::Compact)
 }
 
 pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
     panel_icon_button(id, icon).style(ui::ButtonStyle::Filled)
 }
+
+pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
+    v_flex()
+        .size_full()
+        .gap(px(8.))
+        .p_2()
+        .bg(cx.theme().colors().editor_background)
+}
+
+pub fn panel_editor_style(monospace: bool, window: &mut Window, cx: &mut App) -> EditorStyle {
+    let settings = ThemeSettings::get_global(cx);
+
+    let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
+
+    let (font_family, font_features, font_weight, line_height) = if monospace {
+        (
+            settings.buffer_font.family.clone(),
+            settings.buffer_font.features.clone(),
+            settings.buffer_font.weight,
+            font_size * settings.buffer_line_height.value(),
+        )
+    } else {
+        (
+            settings.ui_font.family.clone(),
+            settings.ui_font.features.clone(),
+            settings.ui_font.weight,
+            window.line_height(),
+        )
+    };
+
+    EditorStyle {
+        background: cx.theme().colors().editor_background,
+        local_player: cx.theme().players().local(),
+        text: TextStyle {
+            color: cx.theme().colors().text,
+            font_family,
+            font_features,
+            font_size: TextSize::Small.rems(cx).into(),
+            font_weight,
+            line_height: line_height.into(),
+            ..Default::default()
+        },
+        ..Default::default()
+    }
+}
+
+pub fn panel_editor_element(
+    editor: &Entity<Editor>,
+    monospace: bool,
+    window: &mut Window,
+    cx: &mut App,
+) -> EditorElement {
+    EditorElement::new(editor, panel_editor_style(monospace, window, cx))
+}

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

@@ -95,7 +95,7 @@ pub struct Button {
     selected_icon: Option<IconName>,
     selected_icon_color: Option<Color>,
     key_binding: Option<KeyBinding>,
-    keybinding_position: KeybindingPosition,
+    key_binding_position: KeybindingPosition,
     alpha: Option<f32>,
 }
 
@@ -121,7 +121,7 @@ impl Button {
             selected_icon: None,
             selected_icon_color: None,
             key_binding: None,
-            keybinding_position: KeybindingPosition::default(),
+            key_binding_position: KeybindingPosition::default(),
             alpha: None,
         }
     }
@@ -197,7 +197,7 @@ impl Button {
     /// This method allows you to specify where the keybinding should be displayed
     /// in relation to the button's label.
     pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
-        self.keybinding_position = position;
+        self.key_binding_position = position;
         self
     }
 
@@ -427,7 +427,7 @@ impl RenderOnce for Button {
                 .child(
                     h_flex()
                         .when(
-                            self.keybinding_position == KeybindingPosition::Start,
+                            self.key_binding_position == KeybindingPosition::Start,
                             |this| this.flex_row_reverse(),
                         )
                         .gap(DynamicSpacing::Base06.rems(cx))

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

@@ -506,7 +506,9 @@ impl RenderOnce for ButtonLike {
             .group("")
             .flex_none()
             .h(self.height.unwrap_or(self.size.rems().into()))
-            .when_some(self.width, |this, width| this.w(width).justify_center())
+            .when_some(self.width, |this, width| {
+                this.w(width).justify_center().text_center()
+            })
             .when_some(self.rounding, |this, rounding| match rounding {
                 ButtonLikeRounding::All => this.rounded_md(),
                 ButtonLikeRounding::Left => this.rounded_l_md(),

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

@@ -22,6 +22,7 @@ pub struct IconButton {
     icon_size: IconSize,
     icon_color: Color,
     selected_icon: Option<IconName>,
+    selected_icon_color: Option<Color>,
     indicator: Option<Indicator>,
     indicator_border_color: Option<Hsla>,
     alpha: Option<f32>,
@@ -36,6 +37,7 @@ impl IconButton {
             icon_size: IconSize::default(),
             icon_color: Color::Default,
             selected_icon: None,
+            selected_icon_color: None,
             indicator: None,
             indicator_border_color: None,
             alpha: None,
@@ -69,6 +71,12 @@ impl IconButton {
         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();
+        self
+    }
+
     pub fn indicator(mut self, indicator: Indicator) -> Self {
         self.indicator = Some(indicator);
         self
@@ -181,6 +189,7 @@ impl RenderOnce for IconButton {
                     .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)

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

@@ -450,6 +450,64 @@ impl RenderOnce for Switch {
     }
 }
 
+/// A [`Switch`] that has a [`Label`].
+#[derive(IntoElement)]
+// #[component(scope = "input")]
+pub struct SwitchWithLabel {
+    id: ElementId,
+    label: Label,
+    toggle_state: ToggleState,
+    on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
+    disabled: bool,
+}
+
+impl SwitchWithLabel {
+    /// Creates a switch with an attached label.
+    pub fn new(
+        id: impl Into<ElementId>,
+        label: Label,
+        toggle_state: impl Into<ToggleState>,
+        on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            label,
+            toggle_state: toggle_state.into(),
+            on_click: Arc::new(on_click),
+            disabled: false,
+        }
+    }
+
+    /// Sets the disabled state of the [`SwitchWithLabel`].
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+}
+
+impl RenderOnce for SwitchWithLabel {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .id(SharedString::from(format!("{}-container", self.id)))
+            .gap(DynamicSpacing::Base08.rems(cx))
+            .child(
+                Switch::new(self.id.clone(), self.toggle_state)
+                    .disabled(self.disabled)
+                    .on_click({
+                        let on_click = self.on_click.clone();
+                        move |checked, window, cx| {
+                            (on_click)(checked, window, cx);
+                        }
+                    }),
+            )
+            .child(
+                div()
+                    .id(SharedString::from(format!("{}-label", self.id)))
+                    .child(self.label),
+            )
+    }
+}
+
 impl ComponentPreview for Checkbox {
     fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
         v_flex()