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

- 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
This commit is contained in:
Nate Butler 2025-02-10 10:52:09 -05:00 committed by GitHub
parent 69d415c8d0
commit de8d4d00ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 586 additions and 120 deletions

3
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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))
}
}

View file

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

View file

@ -1,5 +1,8 @@
//! # panel //! # panel
use gpui::actions; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{actions, Entity, TextStyle};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, Tab}; use ui::{prelude::*, Tab};
actions!(panel, [NextPanelTab, PreviousPanelTab]); 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()); let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
ui::Button::new(id, label) ui::Button::new(id, label)
.label_size(ui::LabelSize::Small) .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) .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 { pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
let id = ElementId::Name(id.into()); let id = ElementId::Name(id.into());
ui::IconButton::new(id, icon) 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) .size(ui::ButtonSize::Compact)
} }
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton { pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
panel_icon_button(id, icon).style(ui::ButtonStyle::Filled) 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))
}

View file

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

View file

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

View file

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

View file

@ -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 { impl ComponentPreview for Checkbox {
fn preview(_window: &mut Window, _cx: &App) -> AnyElement { fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
v_flex() v_flex()