From 6d81ad1e0b5ed395d47a3f221f8a185a522c7375 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 5 Feb 2025 13:54:14 -0500 Subject: [PATCH] git_ui: Start unifying panel style with other panels (#24296) - Adds the `panel` crate for defining UI shared between panels, like common button and header designs, etc - Starts to update the git ui to be more consistent with other panels Release Notes: - N/A --- Cargo.lock | 10 ++++ Cargo.toml | 5 +- crates/git_ui/Cargo.toml | 5 +- crates/git_ui/src/git_panel.rs | 62 +++++++++++++--------- crates/panel/Cargo.toml | 21 ++++++++ crates/panel/LICENSE-GPL | 1 + crates/panel/src/panel.rs | 66 +++++++++++++++++++++++ crates/workspace/src/workspace.rs | 3 +- script/new-crate | 87 +++++++++++++++++++++++++++++++ 9 files changed, 230 insertions(+), 30 deletions(-) create mode 100644 crates/panel/Cargo.toml create mode 120000 crates/panel/LICENSE-GPL create mode 100644 crates/panel/src/panel.rs create mode 100755 script/new-crate diff --git a/Cargo.lock b/Cargo.lock index dadaeeaba1..c679633fae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5245,6 +5245,7 @@ dependencies = [ "language", "menu", "multi_buffer", + "panel", "picker", "postage", "project", @@ -8821,6 +8822,15 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "panel" +version = "0.1.0" +dependencies = [ + "gpui", + "ui", + "workspace", +] + [[package]] name = "parity-tokio-ipc" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index a0e80d7392..17e865c054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ members = [ "crates/open_ai", "crates/outline", "crates/outline_panel", + "crates/panel", "crates/paths", "crates/picker", "crates/prettier", @@ -103,7 +104,6 @@ members = [ "crates/remote_server", "crates/repl", "crates/reqwest_client", - "crates/reqwest_client", "crates/rich_text", "crates/rope", "crates/rpc", @@ -243,8 +243,8 @@ 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" } +git_ui = { path = "crates/git_ui" } go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } gpui = { path = "crates/gpui", default-features = false, features = [ @@ -285,6 +285,7 @@ open_ai = { path = "crates/open_ai" } outline = { path = "crates/outline" } outline_panel = { path = "crates/outline_panel" } paths = { path = "crates/paths" } +panel = { path = "crates/panel" } picker = { path = "crates/picker" } plugin = { path = "crates/plugin" } plugin_macros = { path = "crates/plugin_macros" } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 701f9a01d7..8a2519b8c0 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -22,8 +22,10 @@ futures.workspace = true git.workspace = true gpui.workspace = true language.workspace = true -multi_buffer.workspace = true menu.workspace = true +multi_buffer.workspace = true +panel.workspace = true +picker.workspace = true postage.workspace = true project.workspace = true schemars.workspace = true @@ -35,7 +37,6 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -picker.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 9cf054467d..49b12c0a3f 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -16,6 +16,7 @@ use git::{CommitAllChanges, CommitChanges, ToggleStaged}; use gpui::*; use language::Buffer; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; +use panel::PanelHeader; use project::git::{GitEvent, Repository}; use project::{Fs, Project, ProjectPath}; use serde::{Deserialize, Serialize}; @@ -1060,6 +1061,10 @@ impl GitPanel { .style(ButtonStyle::Filled) } + pub fn indent_size(&self, window: &Window, cx: &mut Context) -> Pixels { + Checkbox::container_size(cx).to_pixels(window.rem_size()) + } + pub fn render_divider(&self, _cx: &mut Context) -> impl IntoElement { h_flex() .items_center() @@ -1069,7 +1074,7 @@ impl GitPanel { pub fn render_panel_header( &self, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let all_repositories = self @@ -1089,11 +1094,7 @@ impl GitPanel { n => format!("{} changes", n), }; - h_flex() - .h(px(32.)) - .items_center() - .px_2() - .bg(ElevationIndex::Surface.bg(cx)) + self.panel_header_container(window, cx) .child(h_flex().gap_2().child(if all_repositories.len() <= 1 { div() .id("changes-label") @@ -1304,7 +1305,12 @@ impl GitPanel { ) } - fn render_entries(&self, has_write_access: bool, cx: &mut Context) -> impl IntoElement { + fn render_entries( + &self, + has_write_access: bool, + window: &Window, + cx: &mut Context, + ) -> impl IntoElement { let entry_count = self.entries.len(); v_flex() @@ -1312,19 +1318,26 @@ impl GitPanel { .overflow_hidden() .child( uniform_list(cx.entity().clone(), "entries", entry_count, { - move |this, range, _window, cx| { + move |this, range, window, cx| { let mut items = Vec::with_capacity(range.end - range.start); for ix in range { match &this.entries.get(ix) { Some(GitListEntry::GitStatusEntry(entry)) => { - items.push(this.render_entry(ix, entry, has_write_access, cx)); + items.push(this.render_entry( + ix, + entry, + has_write_access, + window, + cx, + )); } Some(GitListEntry::Header(header)) => { - items.push(this.render_header( + items.push(this.render_list_header( ix, header, has_write_access, + window, cx, )); } @@ -1338,7 +1351,7 @@ impl GitPanel { .with_decoration( ui::indent_guides( cx.entity().clone(), - px(10.0), + self.indent_size(window, cx), IndentGuideColors::panel(cx), |this, range, _windows, _cx| { this.entries @@ -1353,12 +1366,9 @@ impl GitPanel { ) .with_render_fn( cx.entity().clone(), - move |_, params, window, cx| { - let left_offset = Checkbox::container_size(cx) - .to_pixels(window.rem_size()) - .half(); - const PADDING_Y: f32 = 4.; + move |_, params, _, _| { let indent_size = params.indent_size; + let left_offset = indent_size - px(3.0); let item_height = params.item_height; params @@ -1369,7 +1379,7 @@ impl GitPanel { let offset = if layout.continues_offscreen { px(0.) } else { - px(PADDING_Y) + px(4.0) }; let bounds = Bounds::new( point( @@ -1405,11 +1415,12 @@ impl GitPanel { Label::new(label.into()).color(color).single_line() } - fn render_header( + fn render_list_header( &self, ix: usize, header: &GitHeaderEntry, has_write_access: bool, + _window: &Window, cx: &Context, ) -> AnyElement { let checkbox = Checkbox::new(header.title(), self.header_state(header.header)) @@ -1420,7 +1431,6 @@ impl GitPanel { div() .w_full() - .px_0p5() .child( ListHeader::new(header.title()) .start_slot(checkbox) @@ -1438,7 +1448,8 @@ impl GitPanel { cx, ) }) - }), + }) + .inset(true), ) .into_any_element() } @@ -1448,6 +1459,7 @@ impl GitPanel { ix: usize, entry: &GitStatusEntry, has_write_access: bool, + window: &Window, cx: &Context, ) -> AnyElement { let display_name = entry @@ -1534,7 +1546,7 @@ impl GitPanel { .child( ListItem::new(id) .indent_level(1) - .indent_step_size(px(10.0)) + .indent_step_size(Checkbox::container_size(cx).to_pixels(window.rem_size())) .spacing(ListItemSpacing::Sparse) .start_slot(start_slot) .toggle_state(selected) @@ -1689,16 +1701,14 @@ impl Render for GitPanel { })) .size_full() .overflow_hidden() - .py_1() .bg(ElevationIndex::Surface.bg(cx)) .child(self.render_panel_header(window, cx)) - .child(self.render_divider(cx)) .child(if has_entries { - self.render_entries(has_write_access, cx).into_any_element() + self.render_entries(has_write_access, window, cx) + .into_any_element() } else { self.render_empty_state(cx).into_any_element() }) - .child(self.render_divider(cx)) .child(self.render_commit_editor(name_and_email, cx)) } } @@ -1761,3 +1771,5 @@ impl Panel for GitPanel { 2 } } + +impl PanelHeader for GitPanel {} diff --git a/crates/panel/Cargo.toml b/crates/panel/Cargo.toml new file mode 100644 index 0000000000..03db05bb0b --- /dev/null +++ b/crates/panel/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "panel" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +name = "panel" +path = "src/panel.rs" + +[dependencies] +gpui.workspace = true +ui.workspace = true +workspace.workspace = true + +[features] +default = [] diff --git a/crates/panel/LICENSE-GPL b/crates/panel/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/panel/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs new file mode 100644 index 0000000000..017a362b0e --- /dev/null +++ b/crates/panel/src/panel.rs @@ -0,0 +1,66 @@ +//! # panel +use gpui::actions; +use ui::{prelude::*, Tab}; + +actions!(panel, [NextPanelTab, PreviousPanelTab]); + +pub trait PanelHeader: workspace::Panel { + fn header_height(&self, cx: &mut App) -> Pixels { + Tab::container_height(cx) + } + + fn panel_header_container(&self, _window: &mut Window, cx: &mut App) -> Div { + h_flex() + .h(self.header_height(cx)) + .w_full() + .px_1() + .flex_none() + .border_b_1() + .border_color(cx.theme().colors().border) + } +} + +/// Implement this trait to enable a panel to have tabs. +pub trait PanelTabs: PanelHeader { + /// Returns the index of the currently selected tab. + fn selected_tab(&self, cx: &mut App) -> usize; + /// Selects the tab at the given index. + fn select_tab(&self, cx: &mut App, index: usize); + /// Moves to the next tab. + fn next_tab(&self, _: NextPanelTab, cx: &mut App) -> Self; + /// Moves to the previous tab. + fn previous_tab(&self, _: PreviousPanelTab, cx: &mut App) -> Self; +} + +#[derive(IntoElement)] +pub struct PanelTab {} + +impl RenderOnce for PanelTab { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + div() + } +} + +pub fn panel_button(label: impl Into) -> ui::Button { + let label = label.into(); + let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); + ui::Button::new(id, label) + .label_size(ui::LabelSize::Small) + .layer(ui::ElevationIndex::Surface) + .size(ui::ButtonSize::Compact) +} + +pub fn panel_filled_button(label: impl Into) -> ui::Button { + panel_button(label).style(ui::ButtonStyle::Filled) +} + +pub fn panel_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { + let id = ElementId::Name(id.into()); + ui::IconButton::new(id, icon) + .layer(ui::ElevationIndex::Surface) + .size(ui::ButtonSize::Compact) +} + +pub fn panel_filled_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { + panel_icon_button(id, icon).style(ui::ButtonStyle::Filled) +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8fd0873d03..0472d1ce98 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -21,7 +21,8 @@ use client::{ }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; -use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; +pub use dock::Panel; +use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; use futures::{ channel::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, diff --git a/script/new-crate b/script/new-crate new file mode 100755 index 0000000000..459384d7ae --- /dev/null +++ b/script/new-crate @@ -0,0 +1,87 @@ +#!/bin/bash + +# Try to make sure we are in the zed repo root +if [ ! -d "crates" ] || [ ! -d "script" ]; then + echo "Error: Run from the \`zed\` repo root" + exit 1 +fi + +if [ ! -f "Cargo.toml" ]; then + echo "Error: Run from the \`zed\` repo root" + exit 1 +fi + +if [ $# -eq 0 ]; then + echo "Usage: $0 [optional_license_flag]" + exit 1 +fi + +CRATE_NAME="$1" + +LICENSE_FLAG=$(echo "${2}" | tr '[:upper:]' '[:lower:]') +if [[ "$LICENSE_FLAG" == *"apache"* ]]; then + LICENSE_MODE="Apache-2.0" + LICENSE_FILE="LICENSE-APACHE" +elif [[ "$LICENSE_FLAG" == *"agpl"* ]]; then + LICENSE_MODE="AGPL-3.0-or-later" + LICENSE_FILE="LICENSE-AGPL" +else + LICENSE_MODE="GPL-3.0-or-later" + LICENSE_FILE="LICENSE" +fi + +if [[ ! "$CRATE_NAME" =~ ^[a-z0-9_]+$ ]]; then + echo "Error: Crate name must be lowercase and contain only alphanumeric characters and underscores" + exit 1 +fi + +CRATE_PATH="crates/$CRATE_NAME" +mkdir -p "$CRATE_PATH/src" + +# Symlink the license +ln -sf "../../../$LICENSE_FILE" "$CRATE_PATH/LICENSE" + +CARGO_TOML_TEMPLATE=$(cat << 'EOF' +[package] +name = "$CRATE_NAME" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "$LICENSE_MODE" + +[lints] +workspace = true + +[lib] +name = "$CRATE_NAME" +path = "src/$CRATE_NAME.rs" + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +ui.workspace = true +util.workspace = true + +# Uncomment other workspace dependencies as needed +# assistant.workspace = true +# client.workspace = true +# project.workspace = true +# settings.workspace = true + +[features] +default = [] +EOF +) + +# Populate template +CARGO_TOML_CONTENT=$(echo "$CARGO_TOML_TEMPLATE" | sed \ + -e "s/\$CRATE_NAME/$CRATE_NAME/g" \ + -e "s/\$LICENSE_MODE/$LICENSE_MODE/g") + +echo "$CARGO_TOML_CONTENT" > "$CRATE_PATH/Cargo.toml" + +echo "//! # $CRATE_NAME" > "$CRATE_PATH/src/$CRATE_NAME.rs" + +echo "Created new crate: $CRATE_NAME in $CRATE_PATH" +echo "License: $LICENSE_MODE (symlinked from $LICENSE_FILE)" +echo "Don't forget to add the new crate to the workspace!"