diff --git a/Cargo.lock b/Cargo.lock
index 9aa17fc358..d8950b856c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5169,6 +5169,17 @@ dependencies = [
"util",
]
+[[package]]
+name = "git_ui"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "serde",
+ "ui",
+ "windows 0.58.0",
+ "workspace",
+]
+
[[package]]
name = "glob"
version = "0.3.1"
@@ -15994,6 +16005,7 @@ dependencies = [
"futures 0.3.31",
"git",
"git_hosting_providers",
+ "git_ui",
"go_to_line",
"gpui",
"http_client",
diff --git a/Cargo.toml b/Cargo.toml
index faade8c3a9..743f6178bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -142,6 +142,7 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
+ "crates/git_ui",
#
# Extensions
@@ -227,6 +228,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
+git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg
new file mode 100644
index 0000000000..db6190a9c8
--- /dev/null
+++ b/assets/icons/git_branch.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/panel_left.svg b/assets/icons/panel_left.svg
new file mode 100644
index 0000000000..2eed26673e
--- /dev/null
+++ b/assets/icons/panel_left.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/panel_right.svg b/assets/icons/panel_right.svg
new file mode 100644
index 0000000000..d29a4a519e
--- /dev/null
+++ b/assets/icons/panel_right.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg
new file mode 100644
index 0000000000..2c1d8afdcb
--- /dev/null
+++ b/assets/icons/square_dot.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg
new file mode 100644
index 0000000000..a9ab42c408
--- /dev/null
+++ b/assets/icons/square_minus.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg
new file mode 100644
index 0000000000..8cbe3dc0e7
--- /dev/null
+++ b/assets/icons/square_plus.svg
@@ -0,0 +1 @@
+
diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs
index f4bebca4d8..b32e56aa5e 100644
--- a/crates/feature_flags/src/feature_flags.rs
+++ b/crates/feature_flags/src/feature_flags.rs
@@ -64,6 +64,11 @@ impl FeatureFlag for ZetaFeatureFlag {
const NAME: &'static str = "zeta";
}
+pub struct GitUiFeatureFlag;
+impl FeatureFlag for GitUiFeatureFlag {
+ const NAME: &'static str = "git-ui";
+}
+
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";
diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml
new file mode 100644
index 0000000000..4875b2afdf
--- /dev/null
+++ b/crates/git_ui/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "git_ui"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+name = "git_ui"
+path = "src/git_ui.rs"
+
+[dependencies]
+gpui.workspace = true
+serde.workspace = true
+workspace.workspace = true
+ui.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+windows.workspace = true
+
+[features]
+default = []
diff --git a/crates/git_ui/LICENSE-GPL b/crates/git_ui/LICENSE-GPL
new file mode 120000
index 0000000000..89e542f750
--- /dev/null
+++ b/crates/git_ui/LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE-GPL
\ No newline at end of file
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
new file mode 100644
index 0000000000..b901414d8c
--- /dev/null
+++ b/crates/git_ui/src/git_panel.rs
@@ -0,0 +1,181 @@
+use gpui::*;
+use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
+use workspace::dock::{DockPosition, Panel, PanelEvent};
+use workspace::Workspace;
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(
+ |workspace: &mut Workspace, _cx: &mut ViewContext| {
+ workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::(cx);
+ });
+ },
+ )
+ .detach();
+}
+
+actions!(git_panel, [Deploy, ToggleFocus]);
+
+#[derive(Clone)]
+pub struct GitPanel {
+ _workspace: WeakView,
+ focus_handle: FocusHandle,
+ width: Option,
+}
+
+impl GitPanel {
+ pub fn load(
+ workspace: WeakView,
+ cx: AsyncWindowContext,
+ ) -> Task>> {
+ cx.spawn(|mut cx| async move {
+ workspace.update(&mut cx, |workspace, cx| {
+ let workspace_handle = workspace.weak_handle();
+
+ cx.new_view(|cx| Self::new(workspace_handle, cx))
+ })
+ })
+ }
+
+ pub fn new(workspace: WeakView, cx: &mut ViewContext) -> Self {
+ Self {
+ _workspace: workspace,
+ focus_handle: cx.focus_handle(),
+ width: Some(px(360.)),
+ }
+ }
+
+ pub fn render_panel_header(&self, cx: &mut ViewContext) -> impl IntoElement {
+ h_flex()
+ .h(px(32.))
+ .items_center()
+ .px_3()
+ .bg(ElevationIndex::Surface.bg(cx))
+ .child(
+ h_flex()
+ .gap_1()
+ .child(Checkbox::new("all-changes", true.into()).disabled(true))
+ .child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
+ )
+ .child(div().flex_grow())
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ IconButton::new("discard-changes", IconName::Undo)
+ .icon_size(IconSize::Small)
+ .disabled(true),
+ )
+ .child(
+ Button::new("stage-all", "Stage All")
+ .label_size(LabelSize::Small)
+ .layer(ElevationIndex::ElevatedSurface)
+ .size(ButtonSize::Compact)
+ .style(ButtonStyle::Filled)
+ .disabled(true),
+ ),
+ )
+ }
+
+ pub fn render_commit_editor(&self, cx: &ViewContext) -> impl IntoElement {
+ div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
+ v_flex()
+ .h_full()
+ .py_2p5()
+ .px_3()
+ .bg(cx.theme().colors().editor_background)
+ .font_buffer(cx)
+ .text_ui_sm(cx)
+ .text_color(cx.theme().colors().text_muted)
+ .child("Add a message")
+ .gap_1()
+ .child(div().flex_grow())
+ .child(
+ h_flex().child(div().gap_1().flex_grow()).child(
+ Button::new("commit", "Commit")
+ .label_size(LabelSize::Small)
+ .layer(ElevationIndex::ElevatedSurface)
+ .size(ButtonSize::Compact)
+ .style(ButtonStyle::Filled)
+ .disabled(true),
+ ),
+ )
+ .cursor(CursorStyle::OperationNotAllowed)
+ .opacity(0.5),
+ )
+ }
+}
+
+impl Render for GitPanel {
+ fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement {
+ v_flex()
+ .key_context("GitPanel")
+ .font_buffer(cx)
+ .py_1()
+ .id("git_panel")
+ .track_focus(&self.focus_handle)
+ .size_full()
+ .overflow_hidden()
+ .bg(ElevationIndex::Surface.bg(cx))
+ .child(self.render_panel_header(cx))
+ .child(
+ h_flex()
+ .items_center()
+ .h(px(8.))
+ .child(Divider::horizontal_dashed().color(DividerColor::Border)),
+ )
+ .child(div().flex_1())
+ .child(
+ h_flex()
+ .items_center()
+ .h(px(8.))
+ .child(Divider::horizontal_dashed().color(DividerColor::Border)),
+ )
+ .child(self.render_commit_editor(cx))
+ }
+}
+
+impl FocusableView for GitPanel {
+ fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter for GitPanel {}
+
+impl Panel for GitPanel {
+ fn persistent_name() -> &'static str {
+ "GitPanel"
+ }
+
+ fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
+ DockPosition::Left
+ }
+
+ fn position_is_valid(&self, position: DockPosition) -> bool {
+ matches!(position, DockPosition::Left | DockPosition::Right)
+ }
+
+ fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {}
+
+ fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
+ self.width.unwrap_or(px(360.))
+ }
+
+ fn set_size(&mut self, size: Option, cx: &mut ViewContext) {
+ self.width = size;
+ cx.notify();
+ }
+
+ fn icon(&self, _cx: &gpui::WindowContext) -> Option {
+ Some(ui::IconName::GitBranch)
+ }
+
+ fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+ Some("Git Panel")
+ }
+
+ fn toggle_action(&self) -> Box {
+ Box::new(ToggleFocus)
+ }
+}
diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs
new file mode 100644
index 0000000000..b23171d722
--- /dev/null
+++ b/crates/git_ui/src/git_ui.rs
@@ -0,0 +1 @@
+pub mod git_panel;
diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs
index fdf9b537bc..774d9e59e4 100644
--- a/crates/ui/src/components/button/button.rs
+++ b/crates/ui/src/components/button/button.rs
@@ -445,7 +445,7 @@ impl ComponentPreview for Button {
"A button allows users to take actions, and make choices, with a single tap."
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![
example_group_with_title(
"Styles",
diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs
index 0a3fc6f650..df2dbe3da2 100644
--- a/crates/ui/src/components/checkbox.rs
+++ b/crates/ui/src/components/checkbox.rs
@@ -118,7 +118,7 @@ impl ComponentPreview for Checkbox {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![
example_group_with_title(
"Default",
@@ -214,7 +214,7 @@ impl ComponentPreview for CheckboxWithLabel {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![example_group(vec![
single_example(
"Unselected",
diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs
index b8ba5b8860..d9d71c3eeb 100644
--- a/crates/ui/src/components/content_group.rs
+++ b/crates/ui/src/components/content_group.rs
@@ -95,7 +95,7 @@ impl ComponentPreview for ContentGroup {
ExampleLabelSide::Bottom
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![example_group(vec![
single_example(
"Default",
diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs
index 71234057b2..aa93350da4 100644
--- a/crates/ui/src/components/divider.rs
+++ b/crates/ui/src/components/divider.rs
@@ -3,6 +3,13 @@ use gpui::{Hsla, IntoElement};
use crate::prelude::*;
+#[derive(Clone, Copy, PartialEq)]
+enum DividerStyle {
+ Solid,
+ Dashed,
+}
+
+#[derive(Clone, Copy, PartialEq)]
enum DividerDirection {
Horizontal,
Vertical,
@@ -27,6 +34,7 @@ impl DividerColor {
#[derive(IntoElement)]
pub struct Divider {
+ style: DividerStyle,
direction: DividerDirection,
color: DividerColor,
inset: bool,
@@ -34,22 +42,17 @@ pub struct Divider {
impl RenderOnce for Divider {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
- div()
- .map(|this| match self.direction {
- DividerDirection::Horizontal => {
- this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
- }
- DividerDirection::Vertical => {
- this.w_px().h_full().when(self.inset, |this| this.my_1p5())
- }
- })
- .bg(self.color.hsla(cx))
+ match self.style {
+ DividerStyle::Solid => self.render_solid(cx).into_any_element(),
+ DividerStyle::Dashed => self.render_dashed(cx).into_any_element(),
+ }
}
}
impl Divider {
pub fn horizontal() -> Self {
Self {
+ style: DividerStyle::Solid,
direction: DividerDirection::Horizontal,
color: DividerColor::default(),
inset: false,
@@ -58,6 +61,25 @@ impl Divider {
pub fn vertical() -> Self {
Self {
+ style: DividerStyle::Solid,
+ direction: DividerDirection::Vertical,
+ color: DividerColor::default(),
+ inset: false,
+ }
+ }
+
+ pub fn horizontal_dashed() -> Self {
+ Self {
+ style: DividerStyle::Dashed,
+ direction: DividerDirection::Horizontal,
+ color: DividerColor::default(),
+ inset: false,
+ }
+ }
+
+ pub fn vertical_dashed() -> Self {
+ Self {
+ style: DividerStyle::Dashed,
direction: DividerDirection::Vertical,
color: DividerColor::default(),
inset: false,
@@ -73,4 +95,49 @@ impl Divider {
self.color = color;
self
}
+
+ pub fn render_solid(self, cx: &WindowContext) -> impl IntoElement {
+ div()
+ .map(|this| match self.direction {
+ DividerDirection::Horizontal => {
+ this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
+ }
+ DividerDirection::Vertical => {
+ this.w_px().h_full().when(self.inset, |this| this.my_1p5())
+ }
+ })
+ .bg(self.color.hsla(cx))
+ }
+
+ // TODO: Use canvas or a shader here
+ // This obviously is a short term approach
+ pub fn render_dashed(self, cx: &WindowContext) -> impl IntoElement {
+ let segment_count = 128;
+ let segment_count_f = segment_count as f32;
+ let segment_min_w = 6.;
+ let base = match self.direction {
+ DividerDirection::Horizontal => h_flex(),
+ DividerDirection::Vertical => v_flex(),
+ };
+ let (w, h) = match self.direction {
+ DividerDirection::Horizontal => (px(segment_min_w), px(1.)),
+ DividerDirection::Vertical => (px(1.), px(segment_min_w)),
+ };
+ let color = self.color.hsla(cx);
+ let total_min_w = segment_min_w * segment_count_f * 2.; // * 2 because of the gap
+
+ base.min_w(px(total_min_w))
+ .map(|this| {
+ if self.direction == DividerDirection::Horizontal {
+ this.w_full().h_px()
+ } else {
+ this.w_px().h_full()
+ }
+ })
+ .gap(px(segment_min_w))
+ .overflow_hidden()
+ .children(
+ (0..segment_count).map(|_| div().flex_grow().flex_shrink_0().w(w).h(h).bg(color)),
+ )
+ }
}
diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs
index eb4dd8a98e..e3b799efe5 100644
--- a/crates/ui/src/components/facepile.rs
+++ b/crates/ui/src/components/facepile.rs
@@ -67,7 +67,7 @@ impl ComponentPreview for Facepile {
\n\nFacepiles are used to display a group of people or things,\
such as a list of participants in a collaboration session."
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
let few_faces: [&'static str; 3] = [
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
"https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 8aaef3a69a..e663e00708 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -200,6 +200,7 @@ pub enum IconName {
GenericRestore,
Github,
Globe,
+ GitBranch,
Hash,
HistoryRerun,
Indicator,
@@ -224,6 +225,8 @@ pub enum IconName {
Option,
PageDown,
PageUp,
+ PanelLeft,
+ PanelRight,
Pencil,
Person,
PhoneIncoming,
@@ -267,6 +270,9 @@ pub enum IconName {
SparkleFilled,
Spinner,
Split,
+ SquareDot,
+ SquareMinus,
+ SquarePlus,
Star,
StarFilled,
Stop,
@@ -497,7 +503,7 @@ impl RenderOnce for IconDecoration {
}
impl ComponentPreview for IconDecoration {
- fn examples(cx: &WindowContext) -> Vec> {
+ fn examples(cx: &mut WindowContext) -> Vec> {
let all_kinds = IconDecorationKind::iter().collect::>();
let examples = all_kinds
@@ -539,7 +545,7 @@ impl RenderOnce for DecoratedIcon {
}
impl ComponentPreview for DecoratedIcon {
- fn examples(cx: &WindowContext) -> Vec> {
+ fn examples(cx: &mut WindowContext) -> Vec> {
let icon_1 = Icon::new(IconName::FileDoc);
let icon_2 = Icon::new(IconName::FileDoc);
let icon_3 = Icon::new(IconName::FileDoc);
@@ -658,7 +664,7 @@ impl RenderOnce for IconWithIndicator {
}
impl ComponentPreview for Icon {
- fn examples(_cx: &WindowContext) -> Vec> {
+ fn examples(_cx: &mut WindowContext) -> Vec> {
let arrow_icons = vec![
IconName::ArrowDown,
IconName::ArrowLeft,
diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs
index b0d5b0d2da..26eebb8568 100644
--- a/crates/ui/src/components/indicator.rs
+++ b/crates/ui/src/components/indicator.rs
@@ -89,7 +89,7 @@ impl ComponentPreview for Indicator {
"An indicator visually represents a status or state."
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![
example_group_with_title(
"Types",
diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs
index 0ef5eda7b7..796250947f 100644
--- a/crates/ui/src/components/table.rs
+++ b/crates/ui/src/components/table.rs
@@ -160,7 +160,7 @@ impl ComponentPreview for Table {
ExampleLabelSide::Top
}
- fn examples(_: &WindowContext) -> Vec> {
+ fn examples(_: &mut WindowContext) -> Vec> {
vec![
example_group(vec![
single_example(
diff --git a/crates/ui/src/traits/component_preview.rs b/crates/ui/src/traits/component_preview.rs
index eefc1e8228..aab01355a1 100644
--- a/crates/ui/src/traits/component_preview.rs
+++ b/crates/ui/src/traits/component_preview.rs
@@ -30,20 +30,20 @@ pub trait ComponentPreview: IntoElement {
ExampleLabelSide::default()
}
- fn examples(_cx: &WindowContext) -> Vec>;
+ fn examples(_cx: &mut WindowContext) -> Vec>;
fn custom_example(_cx: &WindowContext) -> impl Into