Git panel polish (#23144)

Nate Butler created

- Clicking checkbox in the header stages or unstages all changes
- Adds tooltips to header checkbox
- Addis the ability for checkboxes to have tooltips
- Ensure an entry in the list is always selected
- Hide revert all button for now

Release Notes:

- N/A

Change summary

crates/git_ui/src/git_panel.rs     | 91 ++++++++++++++++++++++---------
crates/ui/src/components/toggle.rs | 13 ++++
2 files changed, 75 insertions(+), 29 deletions(-)

Detailed changes

crates/git_ui/src/git_panel.rs 🔗

@@ -503,8 +503,7 @@ impl GitPanel {
             let new_selected_entry = if selected_entry > 0 {
                 selected_entry - 1
             } else {
-                self.selected_entry = Some(item_count - 1);
-                item_count - 1
+                selected_entry
             };
 
             self.selected_entry = Some(new_selected_entry);
@@ -550,7 +549,7 @@ impl GitPanel {
         cx.notify();
     }
 
-    fn select_first_entry(&mut self, cx: &mut ViewContext<Self>) {
+    fn select_first_entry_if_none(&mut self, cx: &mut ViewContext<Self>) {
         if !self.no_entries() && self.selected_entry.is_none() {
             self.selected_entry = Some(0);
             self.scroll_to_selected_entry(cx);
@@ -559,7 +558,7 @@ impl GitPanel {
     }
 
     fn focus_changes_list(&mut self, _: &FocusChanges, cx: &mut ViewContext<Self>) {
-        self.select_first_entry(cx);
+        self.select_first_entry_if_none(cx);
 
         cx.focus_self();
         cx.notify();
@@ -776,6 +775,9 @@ impl GitPanel {
         // Sort entries by path to maintain consistent order
         self.visible_entries
             .sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
+
+        self.select_first_entry_if_none(cx);
+
         cx.notify();
     }
 
@@ -830,41 +832,64 @@ impl GitPanel {
             n => format!("{} changes", n),
         };
 
+        // for our use case treat None as false
+        let all_staged = self.all_staged.unwrap_or(false);
+
         h_flex()
             .h(px(32.))
             .items_center()
-            .px_3()
+            .px_2()
             .bg(ElevationIndex::Surface.bg(cx))
             .child(
                 h_flex()
                     .gap_2()
-                    .child(Checkbox::new(
-                        "all-changes",
-                        self.all_staged
-                            .map_or(ToggleState::Indeterminate, ToggleState::from),
-                    ))
+                    .child(
+                        Checkbox::new(
+                            "all-changes",
+                            if self.no_entries() {
+                                ToggleState::Selected
+                            } else {
+                                self.all_staged
+                                    .map_or(ToggleState::Indeterminate, ToggleState::from)
+                            },
+                        )
+                        .fill()
+                        .elevation(ElevationIndex::Surface)
+                        .tooltip(move |cx| {
+                            if all_staged {
+                                Tooltip::text("Unstage all changes", cx)
+                            } else {
+                                Tooltip::text("Stage all changes", cx)
+                            }
+                        })
+                        .on_click(cx.listener(move |git_panel, _, cx| match all_staged {
+                            true => git_panel.unstage_all(&UnstageAll, cx),
+                            false => git_panel.stage_all(&StageAll, cx),
+                        })),
+                    )
                     .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
             )
             .child(div().flex_grow())
             .child(
                 h_flex()
                     .gap_2()
-                    .child(
-                        IconButton::new("discard-changes", IconName::Undo)
-                            .tooltip({
-                                let focus_handle = focus_handle.clone();
-                                move |cx| {
-                                    Tooltip::for_action_in(
-                                        "Discard all changes",
-                                        &RevertAll,
-                                        &focus_handle,
-                                        cx,
-                                    )
-                                }
-                            })
-                            .icon_size(IconSize::Small)
-                            .disabled(true),
-                    )
+                    // TODO: Re-add once revert all is added
+                    // .child(
+                    //     IconButton::new("discard-changes", IconName::Undo)
+                    //         .tooltip({
+                    //             let focus_handle = focus_handle.clone();
+                    //             move |cx| {
+                    //                 Tooltip::for_action_in(
+                    //                     "Discard all changes",
+                    //                     &RevertAll,
+                    //                     &focus_handle,
+                    //                     cx,
+                    //                 )
+                    //             }
+                    //         })
+                    //         .icon_size(IconSize::Small)
+                    //         .disabled(true),
+                    // )
                     .child(if self.all_staged.unwrap_or(false) {
                         self.panel_button("unstage-all", "Unstage All")
                             .tooltip({
@@ -878,6 +903,11 @@ impl GitPanel {
                                     )
                                 }
                             })
+                            .key_binding(ui::KeyBinding::for_action_in(
+                                &UnstageAll,
+                                &focus_handle,
+                                cx,
+                            ))
                             .on_click(
                                 cx.listener(move |this, _, cx| this.unstage_all(&UnstageAll, cx)),
                             )
@@ -894,6 +924,11 @@ impl GitPanel {
                                     )
                                 }
                             })
+                            .key_binding(ui::KeyBinding::for_action_in(
+                                &StageAll,
+                                &focus_handle,
+                                cx,
+                            ))
                             .on_click(cx.listener(move |this, _, cx| this.stage_all(&StageAll, cx)))
                     }),
             )
@@ -1119,9 +1154,9 @@ impl GitPanel {
             });
 
         if view_mode == GitViewMode::Tree {
-            entry = entry.pl(px(12. + 12. * entry_details.depth as f32))
+            entry = entry.pl(px(8. + 12. * entry_details.depth as f32))
         } else {
-            entry = entry.pl(px(12.))
+            entry = entry.pl(px(8.))
         }
 
         if selected {

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

@@ -1,4 +1,4 @@
-use gpui::{div, hsla, prelude::*, ElementId, Hsla, IntoElement, Styled, WindowContext};
+use gpui::{div, hsla, prelude::*, AnyView, ElementId, Hsla, IntoElement, Styled, WindowContext};
 use std::sync::Arc;
 
 use crate::utils::is_light;
@@ -44,6 +44,7 @@ pub struct Checkbox {
     on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
     filled: bool,
     style: ToggleStyle,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
 }
 
 impl Checkbox {
@@ -56,6 +57,7 @@ impl Checkbox {
             on_click: None,
             filled: false,
             style: ToggleStyle::default(),
+            tooltip: None,
         }
     }
 
@@ -91,6 +93,12 @@ impl Checkbox {
         self.style = ToggleStyle::ElevationBased(elevation);
         self
     }
+
+    /// Sets the tooltip for the checkbox.
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
 }
 
 impl Checkbox {
@@ -176,6 +184,9 @@ impl RenderOnce for Checkbox {
                     this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
                 },
             )
+            .when_some(self.tooltip, |this, tooltip| {
+                this.tooltip(move |cx| tooltip(cx))
+            })
     }
 }