diff --git a/Cargo.lock b/Cargo.lock index 85907443ea35f8..f7412a5fb88f38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,7 +1759,7 @@ dependencies = [ "bitflags 2.8.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -1782,7 +1782,7 @@ dependencies = [ "bitflags 2.8.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "log", "prettyplease", "proc-macro2", @@ -7206,7 +7206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -10316,8 +10316,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes 1.10.0", - "heck 0.4.1", - "itertools 0.10.5", + "heck 0.5.0", + "itertools 0.12.1", "log", "multimap 0.10.0", "once_cell", @@ -10350,7 +10350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.90", @@ -15687,7 +15687,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a263b78406c6c2..889fa2e33e09c4 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -742,6 +742,14 @@ "escape": "git_panel::ToggleFocus" } }, + { + "context": "GitCommit > Editor", + "use_key_equivalents": true, + "bindings": { + "enter": "editor::Newline", + "cmd-enter": "git::Commit" + } + }, { "context": "GitPanel > Editor", "use_key_equivalents": true, diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 211cbcf9266f0a..238ba4ffe5f464 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -293,6 +293,7 @@ "!": "vim::ShellCommand", "i": ["vim::PushObject", { "around": false }], "a": ["vim::PushObject", { "around": true }], + "g r": ["vim::Paste", { "preserve_clipboard": true }], "g c": "vim::ToggleComments", "g q": "vim::Rewrap", "\"": "vim::PushRegister", diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index cb4ada5f08896e..eb154ea0209a9c 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -2227,7 +2227,7 @@ impl PromptEditor { }, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size.into(), + font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), ..Default::default() diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index f0a7a2d5536895..e8b049371b2c1f 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1049,7 +1049,7 @@ impl PromptEditor { }, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size.into(), + font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), ..Default::default() diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index c1764cf30d285b..33232469033705 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -56,7 +56,7 @@ impl EventEmitter for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let mut buttons = Vec::new(); let left_gutter_width = match &self.mode { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 62a37166edb9e0..0e9da50f3192a8 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2458,8 +2458,8 @@ impl CollabPanel { Avatar::new(contact.user.avatar_uri.clone()) .indicator::(if online { Some(AvatarAvailabilityIndicator::new(match busy { - true => ui::Availability::Busy, - false => ui::Availability::Free, + true => ui::CollaboratorAvailability::Busy, + false => ui::CollaboratorAvailability::Free, })) } else { None diff --git a/crates/component/src/component.rs b/crates/component/src/component.rs index 35ab8751e5c196..00c1be661ad8e1 100644 --- a/crates/component/src/component.rs +++ b/crates/component/src/component.rs @@ -173,9 +173,9 @@ pub enum ExampleLabelSide { Left, /// Right side Right, - #[default] /// Top side Top, + #[default] /// Bottom side Bottom, } @@ -200,10 +200,10 @@ impl RenderOnce for ComponentExample { ExampleLabelSide::Top => base.flex_col_reverse(), }; - base.gap_1() + base.gap_2() .p_2() - .text_sm() - .text_color(cx.theme().colors().text) + .text_size(px(10.)) + .text_color(cx.theme().colors().text_muted) .when(self.grow, |this| this.flex_1()) .child(self.element) .child(self.variant_name) @@ -245,12 +245,13 @@ impl RenderOnce for ComponentExampleGroup { .text_color(cx.theme().colors().text_muted) .when(self.grow, |this| this.w_full().flex_1()) .when_some(self.title, |this, title| { - this.gap_4().pb_5().child( + this.gap_4().child( div() .flex() .items_center() .gap_3() - .child(div().h_px().w_4().bg(cx.theme().colors().border_variant)) + .pb_1() + .child(div().h_px().w_4().bg(cx.theme().colors().border)) .child( div() .flex_none() @@ -271,7 +272,7 @@ impl RenderOnce for ComponentExampleGroup { .flex() .items_start() .w_full() - .gap_8() + .gap_6() .children(self.examples) .into_any_element(), ) diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index e8edd392fdc551..6a3dcfc406aa17 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -3,8 +3,9 @@ //! A view for exploring Zed components. use component::{components, ComponentMetadata}; -use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window}; -use ui::prelude::*; +use gpui::{list, prelude::*, uniform_list, App, EventEmitter, FocusHandle, Focusable, Window}; +use gpui::{ListState, ScrollHandle, UniformListScrollHandle}; +use ui::{prelude::*, ListItem}; use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId}; @@ -12,7 +13,7 @@ pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _cx| { workspace.register_action( |workspace, _: &workspace::OpenComponentPreview, window, cx| { - let component_preview = cx.new(ComponentPreview::new); + let component_preview = cx.new(|cx| ComponentPreview::new(window, cx)); workspace.add_item_to_active_pane( Box::new(component_preview), None, @@ -28,124 +29,161 @@ pub fn init(cx: &mut App) { struct ComponentPreview { focus_handle: FocusHandle, + _view_scroll_handle: ScrollHandle, + nav_scroll_handle: UniformListScrollHandle, + components: Vec, + component_list: ListState, + selected_index: usize, } impl ComponentPreview { - pub fn new(cx: &mut Context) -> Self { + pub fn new(_window: &mut Window, cx: &mut Context) -> Self { + let components = components().all_sorted(); + let initial_length = components.len(); + + let component_list = ListState::new(initial_length, gpui::ListAlignment::Top, px(500.0), { + let this = cx.entity().downgrade(); + move |ix, window: &mut Window, cx: &mut App| { + this.update(cx, |this, cx| { + this.render_preview(ix, window, cx).into_any_element() + }) + .unwrap() + } + }); + Self { focus_handle: cx.focus_handle(), + _view_scroll_handle: ScrollHandle::new(), + nav_scroll_handle: UniformListScrollHandle::new(), + components, + component_list, + selected_index: 0, } } - fn render_sidebar(&self, _window: &Window, _cx: &Context) -> impl IntoElement { - let components = components().all_sorted(); - let sorted_components = components.clone(); + fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context) { + self.component_list.scroll_to_reveal_item(ix); + self.selected_index = ix; + cx.notify(); + } - v_flex() - .max_w_48() - .gap_px() - .p_1() - .children( - sorted_components - .into_iter() - .map(|component| self.render_sidebar_entry(&component, _cx)), - ) - .child( - Label::new("These will be clickable once the layout is moved to a gpui::List.") - .color(Color::Muted) - .size(LabelSize::XSmall) - .italic(), - ) + fn get_component(&self, ix: usize) -> ComponentMetadata { + self.components[ix].clone() } fn render_sidebar_entry( &self, - component: &ComponentMetadata, - _cx: &Context, + ix: usize, + selected: bool, + cx: &Context, ) -> impl IntoElement { - h_flex() - .w_40() - .px_1p5() - .py_0p5() - .text_sm() - .child(component.name().clone()) + let component = self.get_component(ix); + + ListItem::new(ix) + .child(Label::new(component.name().clone()).color(Color::Default)) + .selectable(true) + .toggle_state(selected) + .inset(true) + .on_click(cx.listener(move |this, _, _, cx| { + this.scroll_to_preview(ix, cx); + })) } fn render_preview( &self, - component: &ComponentMetadata, + ix: usize, window: &mut Window, cx: &Context, ) -> impl IntoElement { + let component = self.get_component(ix); + let name = component.name(); let scope = component.scope(); let description = component.description(); v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .w_full() - .gap_3() - .py_6() + .py_2() .child( v_flex() - .gap_1() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .w_full() + .gap_4() + .py_4() + .px_6() + .flex_none() .child( - h_flex() + v_flex() .gap_1() - .text_2xl() - .child(div().child(name)) - .when_some(scope, |this, scope| { - this.child(div().opacity(0.5).child(format!("({})", scope))) + .child( + h_flex() + .gap_1() + .text_xl() + .child(div().child(name)) + .when_some(scope, |this, scope| { + this.child(div().opacity(0.5).child(format!("({})", scope))) + }), + ) + .when_some(description, |this, description| { + this.child( + div() + .text_ui_sm(cx) + .text_color(cx.theme().colors().text_muted) + .max_w(px(600.0)) + .child(description), + ) }), ) - .when_some(description, |this, description| { - this.child( - div() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .max_w(px(600.0)) - .child(description), - ) + .when_some(component.preview(), |this, preview| { + this.child(preview(window, cx)) }), ) - .when_some(component.preview(), |this, preview| { - this.child(preview(window, cx)) - }) .into_any_element() } - - fn render_previews(&self, window: &mut Window, cx: &Context) -> impl IntoElement { - v_flex() - .id("component-previews") - .size_full() - .overflow_y_scroll() - .p_4() - .gap_4() - .children( - components() - .all_previews_sorted() - .iter() - .map(|component| self.render_preview(component, window, cx)), - ) - } } impl Render for ComponentPreview { - fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { h_flex() .id("component-preview") .key_context("ComponentPreview") .items_start() .overflow_hidden() .size_full() - .max_h_full() .track_focus(&self.focus_handle) .px_2() .bg(cx.theme().colors().editor_background) - .child(self.render_sidebar(window, cx)) - .child(self.render_previews(window, cx)) + .child( + uniform_list( + cx.entity().clone(), + "component-nav", + self.components.len(), + move |this, range, _window, cx| { + range + .map(|ix| this.render_sidebar_entry(ix, ix == this.selected_index, cx)) + .collect() + }, + ) + .track_scroll(self.nav_scroll_handle.clone()) + .pt_4() + .w(px(240.)) + .h_full() + .flex_grow(), + ) + .child( + v_flex() + .id("component-list") + .px_8() + .pt_4() + .size_full() + .child( + list(self.component_list.clone()) + .flex_grow() + .with_sizing_behavior(gpui::ListSizingBehavior::Auto), + ), + ) } } @@ -175,13 +213,13 @@ impl Item for ComponentPreview { fn clone_on_split( &self, _workspace_id: Option, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) -> Option> where Self: Sized, { - Some(cx.new(Self::new)) + Some(cx.new(|cx| Self::new(window, cx))) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { diff --git a/crates/editor/src/commit_tooltip.rs b/crates/editor/src/commit_tooltip.rs index 4bcb73f8ca5232..b9a3a444f72a0b 100644 --- a/crates/editor/src/commit_tooltip.rs +++ b/crates/editor/src/commit_tooltip.rs @@ -210,7 +210,7 @@ impl Render for CommitTooltip { .as_ref() .and_then(|details| details.pull_request.clone()); - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4); tooltip_container(window, cx, move |this, _, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba000921e33482..a3a4dfd6bbdb5c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12769,7 +12769,7 @@ impl Editor { self.toggle_diff_hunks_in_ranges(ranges, cx); } - fn diff_hunks_in_ranges<'a>( + pub fn diff_hunks_in_ranges<'a>( &'a self, ranges: &'a [Range], buffer: &'a MultiBufferSnapshot, @@ -12814,9 +12814,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let head = self.selections.newest_anchor().head(); - self.stage_or_unstage_diff_hunks(true, &[head..head], cx); - self.go_to_next_hunk(&Default::default(), window, cx); + self.do_stage_or_unstage_and_next(true, window, cx); } pub fn unstage_and_next( @@ -12825,9 +12823,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let head = self.selections.newest_anchor().head(); - self.stage_or_unstage_diff_hunks(false, &[head..head], cx); - self.go_to_next_hunk(&Default::default(), window, cx); + self.do_stage_or_unstage_and_next(false, window, cx); } pub fn stage_or_unstage_diff_hunks( @@ -12849,6 +12845,43 @@ impl Editor { } } + fn do_stage_or_unstage_and_next( + &mut self, + stage: bool, + window: &mut Window, + cx: &mut Context, + ) { + let mut ranges = self.selections.disjoint_anchor_ranges().collect::>(); + if ranges.iter().any(|range| range.start != range.end) { + self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx); + return; + } + + if !self.buffer().read(cx).is_singleton() { + if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) { + ranges = vec![multi_buffer::Anchor::range_in_buffer( + excerpt_id, + buffer.read(cx).remote_id(), + range, + )]; + self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx); + let snapshot = self.buffer().read(cx).snapshot(cx); + let mut point = ranges.last().unwrap().end.to_point(&snapshot); + if point.row < snapshot.max_row().0 { + point.row += 1; + point.column = 0; + point = snapshot.clip_point(point, Bias::Right); + self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| { + s.select_ranges([point..point]); + }) + } + return; + } + } + self.stage_or_unstage_diff_hunks(stage, &ranges[..], cx); + self.go_to_next_hunk(&Default::default(), window, cx); + } + fn do_stage_or_unstage( project: &Entity, stage: bool, diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 6275ec97a18ba0..9e22c643931731 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -125,8 +125,7 @@ impl EditableSettingControl for BufferFontSizeControl { } fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.buffer_font_size + ThemeSettings::get_global(cx).buffer_font_size(cx) } fn apply( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 707c03ed95fda0..c8e609b5a89f20 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4343,21 +4343,26 @@ impl EditorElement { let y = display_row_range.start.as_f32() * line_height + text_hitbox.bounds.top() - scroll_pixel_position.y; - let x = text_hitbox.bounds.right() - px(100.); let mut element = diff_hunk_controls( display_row_range.start.0, + status, multi_buffer_range.clone(), line_height, &editor, cx, ); - element.prepaint_as_root( - gpui::Point::new(x, y), - size(px(100.0), line_height).into(), - window, - cx, - ); + let size = + element.layout_as_root(size(px(100.0), line_height).into(), window, cx); + + let x = text_hitbox.bounds.right() + - self.style.scrollbar_width + - px(10.) + - size.width; + + window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { + element.prepaint(window, cx) + }); controls.push(element); } } @@ -7750,7 +7755,7 @@ impl Element for EditorElement { editor.last_position_map = Some(position_map.clone()) }); - let hunk_controls = self.layout_diff_hunk_controls( + let diff_hunk_controls = self.layout_diff_hunk_controls( start_row..end_row, &row_infos, &text_hitbox, @@ -7790,7 +7795,7 @@ impl Element for EditorElement { visible_cursors, selections, inline_completion_popover, - diff_hunk_controls: hunk_controls, + diff_hunk_controls, mouse_context_menu, test_indicators, code_actions_indicator, @@ -9117,6 +9122,7 @@ mod tests { fn diff_hunk_controls( row: u32, + status: &DiffHunkStatus, hunk_range: Range, line_height: Pixels, editor: &Entity, @@ -9133,62 +9139,66 @@ fn diff_hunk_controls( .rounded_b_lg() .bg(cx.theme().colors().editor_background) .gap_1() + .when(status.secondary == DiffHunkSecondaryStatus::None, |el| { + el.child( + Button::new("unstage", "Unstage") + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Unstage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + false, + &[hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }), + ) + }) + .when(status.secondary != DiffHunkSecondaryStatus::None, |el| { + el.child( + Button::new("stage", "Stage") + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Stage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + true, + &[hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }), + ) + }) .child( - IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, window, cx) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let position = hunk_range.end.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_after_position(&snapshot, position, window, cx); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - .child( - IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPrevHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_position(&snapshot, point, window, cx); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - .child( - IconButton::new("discard", IconName::Undo) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) + Button::new("discard", "Restore") .tooltip({ let focus_handle = editor.focus_handle(cx); move |window, cx| { @@ -9212,5 +9222,71 @@ fn diff_hunk_controls( } }), ) + .when( + !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), + |el| { + el.child( + IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let position = + hunk_range.end.to_point(&snapshot.buffer_snapshot); + editor + .go_to_hunk_after_position(&snapshot, position, window, cx); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPrevHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = + hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_position(&snapshot, point, window, cx); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + }, + ) .into_any_element() } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4d7676a7fdd1b..a343dbf3234047 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -339,7 +339,7 @@ fn show_hover( base_text_style.refine(&TextStyleRefinement { font_family: Some(settings.ui_font.family.clone()), font_fallbacks: settings.ui_font.fallbacks.clone(), - font_size: Some(settings.ui_font_size.into()), + font_size: Some(settings.ui_font_size(cx).into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(gpui::transparent_black()), diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index c75e45c1e4fe72..dbad782766caf0 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -206,7 +206,7 @@ impl Editor { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size.into(), + font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), ..Default::default() diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs new file mode 100644 index 00000000000000..6cc67e11496dbf --- /dev/null +++ b/crates/git_ui/src/commit_modal.rs @@ -0,0 +1,244 @@ +#![allow(unused, dead_code)] + +use crate::git_panel::{commit_message_editor, GitPanel}; +use crate::repository_selector::RepositorySelector; +use anyhow::Result; +use git::Commit; +use language::Buffer; +use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button}; +use settings::Settings; +use theme::ThemeSettings; +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::dock::{Dock, DockPosition, PanelHandle}; +use workspace::{ModalView, Workspace}; + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, window, cx| { + let Some(window) = window else { + return; + }; + CommitModal::register(workspace, window, cx) + }) + .detach(); +} + +pub struct CommitModal { + git_panel: Entity, + commit_editor: Entity, + restore_dock: RestoreDock, +} + +impl Focusable for CommitModal { + fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { + self.commit_editor.focus_handle(cx) + } +} + +impl EventEmitter for CommitModal {} +impl ModalView for CommitModal { + fn on_before_dismiss( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> workspace::DismissDecision { + self.git_panel.update(cx, |git_panel, cx| { + git_panel.set_modal_open(false, cx); + }); + self.restore_dock.dock.update(cx, |dock, cx| { + if let Some(active_index) = self.restore_dock.active_index { + dock.activate_panel(active_index, window, cx) + } + dock.set_open(self.restore_dock.is_open, window, cx) + }); + workspace::DismissDecision::Dismiss(true) + } +} + +struct RestoreDock { + dock: WeakEntity, + is_open: bool, + active_index: Option, +} + +impl CommitModal { + pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { + workspace.register_action(|workspace, _: &Commit, window, cx| { + let Some(git_panel) = workspace.panel::(cx) else { + return; + }; + + let (can_commit, conflict) = git_panel.update(cx, |git_panel, cx| { + let can_commit = git_panel.can_commit(); + let conflict = git_panel.has_unstaged_conflicts(); + (can_commit, conflict) + }); + if !can_commit { + let message = if conflict { + "There are still conflicts. You must stage these before committing." + } else { + "No changes to commit." + }; + let prompt = window.prompt(PromptLevel::Warning, message, None, &["Ok"], cx); + cx.spawn(|_, _| async move { + prompt.await.ok(); + }) + .detach(); + } + + let dock = workspace.dock_at_position(git_panel.position(window, cx)); + let is_open = dock.read(cx).is_open(); + let active_index = dock.read(cx).active_panel_index(); + let dock = dock.downgrade(); + let restore_dock_position = RestoreDock { + dock, + is_open, + active_index, + }; + workspace.open_panel::(window, cx); + workspace.toggle_modal(window, cx, move |window, cx| { + CommitModal::new(git_panel, restore_dock_position, window, cx) + }) + }); + } + + fn new( + git_panel: Entity, + restore_dock: RestoreDock, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let panel = git_panel.read(cx); + + let commit_editor = git_panel.update(cx, |git_panel, cx| { + git_panel.set_modal_open(true, cx); + let buffer = git_panel.commit_message_buffer(cx).clone(); + let project = git_panel.project.clone(); + cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx)) + }); + + Self { + git_panel, + commit_editor, + restore_dock, + } + } + + pub fn render_commit_editor( + &self, + name_and_email: Option<(SharedString, SharedString)>, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let editor = self.commit_editor.clone(); + + let panel_editor_style = panel_editor_style(true, window, cx); + + let settings = ThemeSettings::get_global(cx); + let line_height = relative(settings.buffer_line_height.value()) + .to_pixels(settings.buffer_font_size(cx).into(), window.rem_size()); + + v_flex() + .justify_between() + .relative() + .w_full() + .h_full() + .pt_2() + .bg(cx.theme().colors().editor_background) + .child(EditorElement::new(&self.commit_editor, panel_editor_style)) + .child(self.render_footer(window, cx)) + } + + pub fn render_footer(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let (branch, tooltip, title, co_authors) = self.git_panel.update(cx, |git_panel, cx| { + let branch = git_panel + .active_repository + .as_ref() + .and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone())) + .unwrap_or_else(|| "".into()); + let tooltip = if git_panel.has_staged_changes() { + "Commit staged changes" + } else { + "Commit changes to tracked files" + }; + let title = if git_panel.has_staged_changes() { + "Commit" + } else { + "Commit All" + }; + let co_authors = git_panel.render_co_authors(cx); + (branch, tooltip, title, co_authors) + }); + + let branch_selector = Button::new("branch-selector", branch) + .color(Color::Muted) + .style(ButtonStyle::Subtle) + .icon(IconName::GitBranch) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::for_action_title( + "Switch Branch", + &zed_actions::git::Branch, + )) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); + })) + .style(ButtonStyle::Transparent); + h_flex() + .w_full() + .justify_between() + .child(branch_selector) + .child( + h_flex().children(co_authors).child( + panel_filled_button(title) + .tooltip(Tooltip::for_action_title(tooltip, &git::Commit)) + .on_click(cx.listener(|this, _, window, cx| { + this.commit(&Default::default(), window, cx); + })), + ), + ) + } + + fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } + fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context) { + self.git_panel + .update(cx, |git_panel, cx| git_panel.commit_changes(window, cx)); + cx.emit(DismissEvent); + } +} + +impl Render for CommitModal { + fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { + v_flex() + .id("commit-modal") + .key_context("GitCommit") + .elevation_3(cx) + .on_action(cx.listener(Self::dismiss)) + .on_action(cx.listener(Self::commit)) + .relative() + .bg(cx.theme().colors().editor_background) + .rounded(px(16.)) + .border_1() + .border_color(cx.theme().colors().border) + .py_2() + .px_4() + .w(px(480.)) + .min_h(rems(18.)) + .flex_1() + .overflow_hidden() + .child( + v_flex() + .flex_1() + .child(self.render_commit_editor(None, window, cx)), + ) + } +} diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ad2884543be509..dbad09afdacdc7 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,4 +1,5 @@ use crate::git_panel_settings::StatusStyle; +use crate::project_diff::Diff; use crate::repository_selector::RepositorySelectorPopoverMenu; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, @@ -78,16 +79,16 @@ pub fn init(cx: &mut App) { workspace.toggle_panel_focus::(window, cx); }); - workspace.register_action(|workspace, _: &Commit, window, cx| { - workspace.open_panel::(window, cx); - if let Some(git_panel) = workspace.panel::(cx) { - git_panel - .read(cx) - .commit_editor - .focus_handle(cx) - .focus(window); - } - }); + // workspace.register_action(|workspace, _: &Commit, window, cx| { + // workspace.open_panel::(window, cx); + // if let Some(git_panel) = workspace.panel::(cx) { + // git_panel + // .read(cx) + // .commit_editor + // .focus_handle(cx) + // .focus(window); + // } + // }); }, ) .detach(); @@ -174,7 +175,7 @@ struct PendingOperation { } pub struct GitPanel { - active_repository: Option>, + pub(crate) active_repository: Option>, commit_editor: Entity, conflicted_count: usize, conflicted_staged_count: usize, @@ -190,7 +191,7 @@ pub struct GitPanel { pending: Vec, pending_commit: Option>, pending_serialization: Task>, - project: Entity, + pub(crate) project: Entity, repository_selector: Entity, scroll_handle: UniformListScrollHandle, scrollbar_state: ScrollbarState, @@ -202,17 +203,20 @@ pub struct GitPanel { width: Option, workspace: WeakEntity, context_menu: Option<(Entity, Point, Subscription)>, + modal_open: bool, } -fn commit_message_editor( +pub(crate) fn commit_message_editor( commit_message_buffer: Entity, project: Entity, + in_panel: bool, window: &mut Window, cx: &mut Context<'_, Editor>, ) -> Editor { let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); + let max_lines = if in_panel { 6 } else { 18 }; let mut commit_editor = Editor::new( - EditorMode::AutoHeight { max_lines: 6 }, + EditorMode::AutoHeight { max_lines }, buffer, None, false, @@ -251,8 +255,9 @@ impl GitPanel { // just to let us render a placeholder editor. // Once the active git repo is set, this buffer will be replaced. let temporary_buffer = cx.new(|cx| Buffer::local("", cx)); - let commit_editor = - cx.new(|cx| commit_message_editor(temporary_buffer, project.clone(), window, cx)); + let commit_editor = cx.new(|cx| { + commit_message_editor(temporary_buffer, project.clone(), true, window, cx) + }); commit_editor.update(cx, |editor, cx| { editor.clear(window, cx); }); @@ -309,6 +314,7 @@ impl GitPanel { width: Some(px(360.)), context_menu: None, workspace, + modal_open: false, }; git_panel.schedule_update(false, window, cx); git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); @@ -351,6 +357,11 @@ impl GitPanel { ); } + pub(crate) fn set_modal_open(&mut self, open: bool, cx: &mut Context) { + self.modal_open = open; + cx.notify(); + } + fn dispatch_context(&self, window: &mut Window, cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("GitPanel"); @@ -592,7 +603,6 @@ impl GitPanel { }) .ok() }); - self.focus_handle.focus(window); } fn open_file( @@ -998,6 +1008,16 @@ impl GitPanel { .detach(); } + pub fn commit_message_buffer(&self, cx: &App) -> Entity { + self.commit_editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .clone() + } + fn toggle_staged_for_selected( &mut self, _: &git::ToggleStaged, @@ -1022,7 +1042,7 @@ impl GitPanel { self.commit_changes(window, cx) } - fn commit_changes(&mut self, window: &mut Window, cx: &mut Context) { + pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context) { let Some(active_repository) = self.active_repository.clone() else { return; }; @@ -1288,7 +1308,7 @@ impl GitPanel { != Some(&buffer) { git_panel.commit_editor = cx.new(|cx| { - commit_message_editor(buffer, git_panel.project.clone(), window, cx) + commit_message_editor(buffer, git_panel.project.clone(), true, window, cx) }); } }) @@ -1476,12 +1496,18 @@ impl GitPanel { entry.is_staged } - fn has_staged_changes(&self) -> bool { + pub(crate) fn has_staged_changes(&self) -> bool { self.tracked_staged_count > 0 || self.new_staged_count > 0 || self.conflicted_staged_count > 0 } + pub(crate) fn has_unstaged_changes(&self) -> bool { + self.tracked_count > self.tracked_staged_count + || self.new_count > self.new_staged_count + || self.conflicted_count > self.conflicted_staged_count + } + fn has_conflicts(&self) -> bool { self.conflicted_count > 0 } @@ -1490,7 +1516,7 @@ impl GitPanel { self.tracked_count > 0 } - fn has_unstaged_conflicts(&self) -> bool { + pub fn has_unstaged_conflicts(&self) -> bool { self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count } @@ -1564,7 +1590,17 @@ impl GitPanel { .size(LabelSize::Small) .color(Color::Muted), ) - .child(self.render_repository_selector(cx)), + .child(self.render_repository_selector(cx)) + .child(div().flex_grow()) + .child( + Button::new("diff", "+/-") + .tooltip(Tooltip::for_action_title("Open diff", &Diff)) + .on_click(|_, _, cx| { + cx.defer(|cx| { + cx.dispatch_action(&Diff); + }) + }), + ), ) } else { None @@ -1587,21 +1623,64 @@ impl GitPanel { ) } + pub fn can_commit(&self) -> bool { + (self.has_staged_changes() || self.has_tracked_changes()) && !self.has_unstaged_conflicts() + } + + pub fn can_stage_all(&self) -> bool { + self.has_unstaged_changes() + } + + pub fn can_unstage_all(&self) -> bool { + self.has_staged_changes() + } + + pub(crate) fn render_co_authors(&self, cx: &Context) -> Option { + let potential_co_authors = self.potential_co_authors(cx); + if potential_co_authors.is_empty() { + None + } else { + Some( + IconButton::new("co-authors", IconName::Person) + .icon_color(Color::Disabled) + .selected_icon_color(Color::Selected) + .toggle_state(self.add_coauthors) + .tooltip(move |_, cx| { + let title = format!( + "Add co-authored-by:{}{}", + if potential_co_authors.len() == 1 { + "" + } else { + "\n" + }, + potential_co_authors + .iter() + .map(|(name, email)| format!(" {} <{}>", name, email)) + .join("\n") + ); + Tooltip::simple(title, cx) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.add_coauthors = !this.add_coauthors; + cx.notify(); + })) + .into_any_element(), + ) + } + } + pub fn render_commit_editor( &self, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let editor = self.commit_editor.clone(); - let can_commit = (self.has_staged_changes() || self.has_tracked_changes()) + let can_commit = self.can_commit() && 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 enable_coauthors = self.render_co_authors(cx); let editor_focus_handle = editor.read(cx).focus_handle(cx).clone(); @@ -1627,37 +1706,6 @@ impl GitPanel { cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx)) }); - let potential_co_authors = self.potential_co_authors(cx); - let enable_coauthors = if potential_co_authors.is_empty() { - None - } else { - Some( - IconButton::new("co-authors", IconName::Person) - .icon_color(Color::Disabled) - .selected_icon_color(Color::Selected) - .toggle_state(self.add_coauthors) - .tooltip(move |_, cx| { - let title = format!( - "Add co-authored-by:{}{}", - if potential_co_authors.len() == 1 { - "" - } else { - "\n" - }, - potential_co_authors - .iter() - .map(|(name, email)| format!(" {} <{}>", name, email)) - .join("\n") - ); - Tooltip::simple(title, cx) - }) - .on_click(cx.listener(|this, _, _, cx| { - this.add_coauthors = !this.add_coauthors; - cx.notify(); - })), - ) - }; - let branch = self .active_repository .as_ref() @@ -1698,26 +1746,28 @@ impl GitPanel { .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(branch_selector), - ) - .child( - h_flex() - .absolute() - .bottom_0() - .right_2() - .h(footer_size) - .flex_none() - .children(enable_coauthors) - .child(commit_button), - ) + .when(!self.modal_open, |el| { + el.child(EditorElement::new(&self.commit_editor, panel_editor_style)) + .child( + h_flex() + .absolute() + .bottom_0() + .left_2() + .h(footer_size) + .flex_none() + .child(branch_selector), + ) + .child( + h_flex() + .absolute() + .bottom_0() + .right_2() + .h(footer_size) + .flex_none() + .children(enable_coauthors) + .child(commit_button), + ) + }) } fn render_previous_commit(&self, cx: &mut Context) -> Option { @@ -1892,8 +1942,8 @@ impl GitPanel { Some( h_flex() .id("start-slot") + .text_lg() .child(checkbox) - .child(git_status_icon(entry.status_entry()?.status, cx)) .on_mouse_down(MouseButton::Left, |_, _, cx| { // prevent the list item active state triggering when toggling checkbox cx.stop_propagation(); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 300c589ecd910c..7cca2b23a59c92 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -6,17 +6,17 @@ use project_diff::ProjectDiff; use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; pub mod branch_picker; +mod commit_modal; 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); + commit_modal::init(cx); } // TODO: Add updated status colors to theme diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 3d092f0d668a65..ebec4d7848877b 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,25 +1,32 @@ use std::any::{Any, TypeId}; +use ::git::UnstageAndNext; use anyhow::Result; -use buffer_diff::BufferDiff; +use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::HashSet; -use editor::{scroll::Autoscroll, Editor, EditorEvent, ToPoint}; +use editor::{ + actions::{GoToHunk, GoToPrevHunk}, + scroll::Autoscroll, + Editor, EditorEvent, ToPoint, +}; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; +use git::{Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll}; use gpui::{ - actions, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, - FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, + actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, + EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point}; use multi_buffer::{MultiBuffer, PathKey}; use project::{git::GitStore, Project, ProjectPath}; use theme::ActiveTheme; -use ui::prelude::*; +use ui::{prelude::*, vertical_divider, Tooltip}; use util::ResultExt as _; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, - ItemNavHistory, SerializableItem, ToolbarItemLocation, Workspace, + ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + Workspace, }; use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}; @@ -197,6 +204,69 @@ impl ProjectDiff { } } + fn button_states(&self, cx: &App) -> ButtonStates { + let editor = self.editor.read(cx); + let snapshot = self.multibuffer.read(cx).snapshot(cx); + let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); + let mut selection = true; + + let mut ranges = editor + .selections + .disjoint_anchor_ranges() + .collect::>(); + if !ranges.iter().any(|range| range.start != range.end) { + selection = false; + if let Some((excerpt_id, buffer, range)) = self.editor.read(cx).active_excerpt(cx) { + ranges = vec![multi_buffer::Anchor::range_in_buffer( + excerpt_id, + buffer.read(cx).remote_id(), + range, + )]; + } else { + ranges = Vec::default(); + } + } + let mut has_staged_hunks = false; + let mut has_unstaged_hunks = false; + for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) { + match hunk.secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk => { + has_unstaged_hunks = true; + } + DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => { + has_staged_hunks = true; + has_unstaged_hunks = true; + } + DiffHunkSecondaryStatus::None => { + has_staged_hunks = true; + } + } + } + let mut commit = false; + let mut stage_all = false; + let mut unstage_all = false; + self.workspace + .read_with(cx, |workspace, cx| { + if let Some(git_panel) = workspace.panel::(cx) { + let git_panel = git_panel.read(cx); + commit = git_panel.can_commit(); + stage_all = git_panel.can_stage_all(); + unstage_all = git_panel.can_unstage_all(); + } + }) + .ok(); + + return ButtonStates { + stage: has_unstaged_hunks, + unstage: has_staged_hunks, + prev_next, + selection, + commit, + stage_all, + unstage_all, + }; + } + fn handle_editor_event( &mut self, editor: &Entity, @@ -598,3 +668,226 @@ impl SerializableItem for ProjectDiff { false } } + +pub struct ProjectDiffToolbar { + project_diff: Option>, + workspace: WeakEntity, +} + +impl ProjectDiffToolbar { + pub fn new(workspace: &Workspace, _: &mut Context) -> Self { + Self { + project_diff: None, + workspace: workspace.weak_handle(), + } + } + + fn project_diff(&self, _: &App) -> Option> { + self.project_diff.as_ref()?.upgrade() + } + fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context) { + if let Some(project_diff) = self.project_diff(cx) { + project_diff.focus_handle(cx).focus(window); + } + let action = action.boxed_clone(); + cx.defer(move |cx| { + cx.dispatch_action(action.as_ref()); + }) + } + fn dispatch_panel_action( + &self, + action: &dyn Action, + window: &mut Window, + cx: &mut Context, + ) { + self.workspace + .read_with(cx, |workspace, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.focus_handle(cx).focus(window) + } + }) + .ok(); + let action = action.boxed_clone(); + cx.defer(move |cx| { + cx.dispatch_action(action.as_ref()); + }) + } +} + +impl EventEmitter for ProjectDiffToolbar {} + +impl ToolbarItemView for ProjectDiffToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + self.project_diff = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|entity| entity.downgrade()); + if self.project_diff.is_some() { + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } + + fn pane_focus_update( + &mut self, + _pane_focused: bool, + _window: &mut Window, + _cx: &mut Context, + ) { + } +} + +struct ButtonStates { + stage: bool, + unstage: bool, + prev_next: bool, + selection: bool, + stage_all: bool, + unstage_all: bool, + commit: bool, +} + +impl Render for ProjectDiffToolbar { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(project_diff) = self.project_diff(cx) else { + return div(); + }; + let focus_handle = project_diff.focus_handle(cx); + let button_states = project_diff.read(cx).button_states(cx); + + h_group_xl() + .my_neg_1() + .items_center() + .py_1() + .pl_2() + .pr_1() + .flex_wrap() + .justify_between() + .child( + h_group_sm() + .when(button_states.selection, |el| { + el.child( + Button::new("stage", "Toggle Staged") + .tooltip(Tooltip::for_action_title_in( + "Toggle Staged", + &ToggleStaged, + &focus_handle, + )) + .disabled(!button_states.stage && !button_states.unstage) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&ToggleStaged, window, cx) + })), + ) + }) + .when(!button_states.selection, |el| { + el.child( + Button::new("stage", "Stage") + .tooltip(Tooltip::for_action_title_in( + "Stage", + &StageAndNext, + &focus_handle, + )) + // don't actually disable the button so it's mashable + .color(if button_states.stage { + Color::Default + } else { + Color::Disabled + }) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&StageAndNext, window, cx) + })), + ) + .child( + Button::new("unstage", "Unstage") + .tooltip(Tooltip::for_action_title_in( + "Unstage", + &UnstageAndNext, + &focus_handle, + )) + .color(if button_states.unstage { + Color::Default + } else { + Color::Disabled + }) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&UnstageAndNext, window, cx) + })), + ) + }), + ) + // n.b. the only reason these arrows are here is because we don't + // support "undo" for staging so we need a way to go back. + .child( + h_group_sm() + .child( + IconButton::new("up", IconName::ArrowUp) + .shape(ui::IconButtonShape::Square) + .tooltip(Tooltip::for_action_title_in( + "Go to previous hunk", + &GoToPrevHunk, + &focus_handle, + )) + .disabled(!button_states.prev_next) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToPrevHunk, window, cx) + })), + ) + .child( + IconButton::new("down", IconName::ArrowDown) + .shape(ui::IconButtonShape::Square) + .tooltip(Tooltip::for_action_title_in( + "Go to next hunk", + &GoToHunk, + &focus_handle, + )) + .disabled(!button_states.prev_next) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_action(&GoToHunk, window, cx) + })), + ), + ) + .child(vertical_divider()) + .child( + h_group_sm() + .when( + button_states.unstage_all && !button_states.stage_all, + |el| { + el.child(Button::new("unstage-all", "Unstage All").on_click( + cx.listener(|this, _, window, cx| { + this.dispatch_panel_action(&UnstageAll, window, cx) + }), + )) + }, + ) + .when( + !button_states.unstage_all || button_states.stage_all, + |el| { + el.child( + // todo make it so that changing to say "Unstaged" + // doesn't change the position. + div().child( + Button::new("stage-all", "Stage All") + .disabled(!button_states.stage_all) + .on_click(cx.listener(|this, _, window, cx| { + this.dispatch_panel_action(&StageAll, window, cx) + })), + ), + ) + }, + ) + .child( + Button::new("commit", "Commit") + .disabled(!button_states.commit) + .on_click(cx.listener(|this, _, window, cx| { + // todo this should open modal, not focus panel. + this.dispatch_action(&Commit, window, cx); + })), + ), + ) + } +} diff --git a/crates/git_ui/src/quick_commit.rs b/crates/git_ui/src/quick_commit.rs index cd8a3154963f66..e69de29bb2d1d6 100644 --- a/crates/git_ui/src/quick_commit.rs +++ b/crates/git_ui/src/quick_commit.rs @@ -1,307 +0,0 @@ -#![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>, - 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, - project: Entity, - active_repository: Option>, - repository_selector: Entity, - commit_editor: Entity, - width: Option, - commit_task: Task>, - 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 for QuickCommitModal {} -impl ModalView for QuickCommitModal {} - -impl QuickCommitModal { - pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { - 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, - fs: Arc, - window: &mut Window, - commit_message_buffer: Option>, - cx: &mut Context, - ) -> Self { - let git_store = project.read(cx).git_store().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) -> impl IntoElement { - let all_repositories = self - .project - .read(cx) - .git_store() - .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, - ) -> 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) -> 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) { - 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)) - } -} diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 20a4bae3a6aa46..c34ca6e7e45e26 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1249,6 +1249,7 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option { VK_ESCAPE => "escape", VK_INSERT => "insert", VK_DELETE => "delete", + VK_APPS => "menu", _ => { let basic_key = basic_vkcode_to_string(vk_code, modifiers); if basic_key.is_some() { @@ -1303,6 +1304,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { VK_ESCAPE => "escape", VK_INSERT => "insert", VK_DELETE => "delete", + VK_APPS => "menu", _ => { if is_modifier(VIRTUAL_KEY(vk_code)) { return Some(KeystrokeOrModifier::Modifier(modifiers)); diff --git a/crates/languages/src/bash/highlights.scm b/crates/languages/src/bash/highlights.scm index 5cb5dad6a00695..a8ae81058a1a33 100644 --- a/crates/languages/src/bash/highlights.scm +++ b/crates/languages/src/bash/highlights.scm @@ -4,11 +4,10 @@ (heredoc_body) (heredoc_start) (ansi_c_string) + (word) ] @string -(command_name) @function - -(variable_name) @property +(variable_name) @variable [ "case" @@ -35,24 +34,67 @@ (comment) @comment (function_definition name: (word) @function) +(command_name (word) @function) + +[ + (file_descriptor) + (number) +] @number -(file_descriptor) @number +(regex) @string.regex [ (command_substitution) (process_substitution) (expansion) -]@embedded +] @embedded + [ "$" "&&" ">" ">>" + ">&" + ">&-" "<" "|" + ":" + "//" + "/" + "%" + "%%" + "#" + "##" + "=" + "==" ] @operator +(test_operator) @keyword.operator + +[ + ";" +] @punctuation.delimiter + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +(simple_expansion + "$" @punctuation.special) +(expansion + "${" @punctuation.special + "}" @punctuation.special) @embedded + +(command_substitution + "$(" @punctuation.special + ")" @punctuation.special) + ( (command (_) @constant) (#match? @constant "^-") diff --git a/crates/languages/src/c/highlights.scm b/crates/languages/src/c/highlights.scm index 634f8d81c4ad93..b80c462ae6d329 100644 --- a/crates/languages/src/c/highlights.scm +++ b/crates/languages/src/c/highlights.scm @@ -102,8 +102,9 @@ [ (true) (false) - (null) -] @constant +] @boolean + +(null) @constant.builtin (identifier) @variable diff --git a/crates/languages/src/cpp/highlights.scm b/crates/languages/src/cpp/highlights.scm index 2df9ec2923be3d..b05e8cd6f97935 100644 --- a/crates/languages/src/cpp/highlights.scm +++ b/crates/languages/src/cpp/highlights.scm @@ -153,9 +153,12 @@ type :(primitive_type) @type.primitive [ (true) (false) +] @boolean + +[ (null) ("nullptr") -] @constant +] @constant.builtin (number_literal) @number diff --git a/crates/languages/src/go/highlights.scm b/crates/languages/src/go/highlights.scm index 609c49c13f53cf..5aa23fca90b7e0 100644 --- a/crates/languages/src/go/highlights.scm +++ b/crates/languages/src/go/highlights.scm @@ -2,6 +2,7 @@ (type_identifier) @type (field_identifier) @variable.member +(package_identifier) @namespace (keyed_element . @@ -20,6 +21,15 @@ (method_declaration name: (field_identifier) @function.method) +(method_elem + name: (field_identifier) @function.method) + +[ + ";" + "." + "," + ":" +] @punctuation.delimiter [ "(" @@ -118,6 +128,9 @@ [ (true) (false) +] @boolean + +[ (nil) (iota) ] @constant.builtin diff --git a/crates/languages/src/json/highlights.scm b/crates/languages/src/json/highlights.scm index 8cf7a6d20dc6c7..1098320ccba78c 100644 --- a/crates/languages/src/json/highlights.scm +++ b/crates/languages/src/json/highlights.scm @@ -11,8 +11,14 @@ [ (true) (false) - (null) -] @constant +] @boolean + +(null) @constant.builtin + +[ + "," + ":" +] @punctuation.delimiter [ "{" diff --git a/crates/languages/src/jsonc/highlights.scm b/crates/languages/src/jsonc/highlights.scm index 8cf7a6d20dc6c7..1098320ccba78c 100644 --- a/crates/languages/src/jsonc/highlights.scm +++ b/crates/languages/src/jsonc/highlights.scm @@ -11,8 +11,14 @@ [ (true) (false) - (null) -] @constant +] @boolean + +(null) @constant.builtin + +[ + "," + ":" +] @punctuation.delimiter [ "{" diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index 0495f87716bd21..e3e38aafedf52a 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -95,9 +95,12 @@ ; Literals [ - (none) (true) (false) +] @boolean + +[ + (none) (ellipsis) ] @constant.builtin diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index ba4e2c3d58f1fa..f9d793105e380b 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -577,6 +577,26 @@ impl ContextProvider for RustContextProvider { cwd: Some("$ZED_DIRNAME".to_owned()), ..TaskTemplate::default() }, + TaskTemplate { + label: format!( + "DocTest '{}' (package: {})", + VariableName::Symbol.template_value(), + RUST_PACKAGE_TASK_VARIABLE.template_value(), + ), + command: "cargo".into(), + args: vec![ + "test".into(), + "--doc".into(), + "-p".into(), + RUST_PACKAGE_TASK_VARIABLE.template_value(), + VariableName::Symbol.template_value(), + "--".into(), + "--nocapture".into(), + ], + tags: vec!["rust-doc-test".to_owned()], + cwd: Some("$ZED_DIRNAME".to_owned()), + ..TaskTemplate::default() + }, TaskTemplate { label: format!( "Test '{}' (package: {})", diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index d25b8a734719ba..5244cd630dc0e9 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -99,6 +99,7 @@ "mod" "move" "pub" + "raw" "ref" "return" "static" @@ -129,7 +130,7 @@ (float_literal) ] @number -(boolean_literal) @constant +(boolean_literal) @boolean [ (line_comment) diff --git a/crates/languages/src/rust/runnables.scm b/crates/languages/src/rust/runnables.scm index 6d8dee4445364a..c962e771174a3a 100644 --- a/crates/languages/src/rust/runnables.scm +++ b/crates/languages/src/rust/runnables.scm @@ -1,4 +1,4 @@ - +; Rust mod test ( (mod_item name: (_) @run @@ -7,6 +7,7 @@ (#set! tag rust-mod-test) ) +; Rust test ( ( (attribute_item (attribute @@ -28,6 +29,45 @@ (#set! tag rust-test) ) +; Rust doc test +( + ( + (line_comment) * + (line_comment + doc: (_) @_comment_content + ) @start + (#match? @_comment_content "```") + (line_comment) * + (line_comment + doc: (_) @_end_comment_content + ) @_end_code_block + (#match? @_end_comment_content "```") + . + (attribute_item) * + . + [(line_comment) (block_comment)] * + . + [(function_item + name: (_) @run + body: _ + ) (function_signature_item + name: (_) @run + ) (struct_item + name: (_) @run + ) (enum_item + name: (_) @run + body: _ + ) ( + (attribute_item) ? + (macro_definition + name: (_) @run) + ) (mod_item + name: (_) @run + )] @_end + ) + (#set! tag rust-doc-test) +) + ; Rust main function ( ( diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 53bb500ac24c3f..fb2ae93799c189 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -791,7 +791,7 @@ impl LanguageServer { }), }, trace: None, - workspace_folders: None, + workspace_folders: Some(vec![]), client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| { ClientInfo { name: release_channel.display_name().to_string(), diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index ea733c213742d7..f30dc190e1f5d1 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -186,7 +186,7 @@ impl SshPrompt { let refinement = TextStyleRefinement { font_family: Some(theme.buffer_font.family.clone()), font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(theme.buffer_font_size.into()), + font_size: Some(theme.buffer_font_size(cx).into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(gpui::transparent_black()), ..Default::default() diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 7658a106e0eacd..d4df0f60f8a7dc 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -186,7 +186,7 @@ impl Cell { let refinement = TextStyleRefinement { font_family: Some(theme.buffer_font.family.clone()), - font_size: Some(theme.buffer_font_size.into()), + font_size: Some(theme.buffer_font_size(cx).into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(gpui::transparent_black()), ..Default::default() diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs index 8d9168234261a2..04eb2cc6e73d4e 100644 --- a/crates/repl/src/outputs/table.rs +++ b/crates/repl/src/outputs/table.rs @@ -94,7 +94,7 @@ impl TableView { let text_system = window.text_system(); let text_style = window.text_style(); let text_font = ThemeSettings::get_global(cx).buffer_font.clone(); - let font_size = ThemeSettings::get_global(cx).buffer_font_size; + let font_size = ThemeSettings::get_global(cx).buffer_font_size(cx); let mut runs = [TextRun { len: 0, font: text_font, diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index fb373667c1ff27..d1cae1d8a77e9c 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -239,8 +239,7 @@ impl EditableSettingControl for UiFontSizeControl { } fn read(cx: &App) -> Self::Value { - let settings = ThemeSettings::get_global(cx); - settings.ui_font_size + ThemeSettings::get_global(cx).ui_font_size(cx) } fn apply( diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 9bae13f7bf1e14..a4814404135389 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -14,8 +14,6 @@ use ui::prelude::*; pub enum ComponentStory { ApplicationMenu, AutoHeightEditor, - Avatar, - Button, CollabNotification, ContextMenu, Cursor, @@ -47,8 +45,6 @@ impl ComponentStory { .new(|cx| title_bar::ApplicationMenuStory::new(window, cx)) .into(), Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(), - Self::Avatar => cx.new(|_| ui::AvatarStory).into(), - Self::Button => cx.new(|_| ui::ButtonStory).into(), Self::CollabNotification => cx .new(|_| collab_ui::notifications::CollabNotificationStory) .into(), diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs index 01b91007145141..e72a1e74197131 100644 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ b/crates/terminal_view/src/terminal_scrollbar.rs @@ -69,9 +69,10 @@ impl ScrollableHandle for TerminalScrollHandle { let offset_delta = (point.y.0 / state.line_height.0).round() as i32; let max_offset = state.total_lines - state.viewport_lines; - let display_offset = ((max_offset as i32 + offset_delta) as usize).min(max_offset); + let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32); - self.future_display_offset.set(Some(display_offset)); + self.future_display_offset + .set(Some(display_offset as usize)); } fn viewport(&self) -> Bounds { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 37359271157045..9b6c6a0c820e70 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -95,13 +95,17 @@ pub struct ThemeSettings { /// as well as the size of a [gpui::Rems] unit. /// /// Changing this will impact the size of all UI elements. - pub ui_font_size: Pixels, + /// + /// Use [ThemeSettings::ui_font_size] to access this. + ui_font_size: Pixels, /// The font used for UI elements. pub ui_font: Font, /// The font size used for buffers, and the terminal. /// /// The terminal font size can be overridden using it's own setting. - pub buffer_font_size: Pixels, + /// + /// Use [ThemeSettings::buffer_font_size] to access this. + buffer_font_size: Pixels, /// The font used for buffers, and the terminal. /// /// The terminal font family can be overridden using it's own setting. @@ -569,6 +573,14 @@ impl ThemeSettings { clamp_font_size(font_size) } + /// Returns the UI font size. + pub fn ui_font_size(&self, cx: &App) -> Pixels { + let font_size = cx + .try_global::() + .map_or(self.ui_font_size, |size| size.0); + clamp_font_size(font_size) + } + // TODO: Rename: `line_height` -> `buffer_line_height` /// Returns the buffer's line height. pub fn line_height(&self) -> f32 { @@ -715,14 +727,14 @@ pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font { /// Gets the adjusted UI font size. pub fn get_ui_font_size(cx: &App) -> Pixels { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); cx.try_global::() .map_or(ui_font_size, |adjusted_size| adjusted_size.0) } /// Sets the adjusted UI font size. pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let mut adjusted_size = cx .try_global::() .map_or(ui_font_size, |adjusted_size| adjusted_size.0); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e62910b6819d8d..bebf6f8aa7b10d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -103,9 +103,9 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) { ThemeSettings::register(cx); FontFamilyCache::init_global(cx); - let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; + let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx); cx.observe_global::(move |cx| { - let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; + let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx); if buffer_font_size != prev_buffer_font_size { prev_buffer_font_size = buffer_font_size; reset_buffer_font_size(cx); diff --git a/crates/ui/docs/building-ui.md b/crates/ui/docs/building-ui.md deleted file mode 100644 index e0160e336ed36d..00000000000000 --- a/crates/ui/docs/building-ui.md +++ /dev/null @@ -1,49 +0,0 @@ -# Building UI with GPUI - -## Common patterns - -### Method ordering - -- id -- Flex properties -- Position properties -- Size properties -- Style properties -- Handlers -- State properties - -### Using the Label Component to Create UI Text - -The `Label` component helps in displaying text on user interfaces. It creates an interface where specific parameters such as label color, line height style, and strikethrough can be set. - -Firstly, to create a `Label` instance, use the `Label::new()` function. This function takes a string that will be displayed as text in the interface. - -```rust -Label::new("Hello, world!"); -``` - -Now let's dive a bit deeper into how to customize `Label` instances: - -- **Setting Color:** To set the color of the label using various predefined color options such as `Default`, `Muted`, `Created`, `Modified`, `Deleted`, etc, the `color()` function is called on the `Label` instance: - - ```rust - Label::new("Hello, world!").color(LabelColor::Default); - ``` - -- **Setting Line Height Style:** To set the line height style, the `line_height_style()` function is utilized: - - ```rust - Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel); - ``` - -- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the `set_strikethrough()` function is used: - - ```rust - Label::new("Hello, world!").set_strikethrough(true); - ``` - -That's it! Now you can use the `Label` component to create and customize text on your application's interface. - -## Building a new component - -TODO diff --git a/crates/ui/docs/hello-world.md b/crates/ui/docs/hello-world.md deleted file mode 100644 index 8ff2fe4db7f1da..00000000000000 --- a/crates/ui/docs/hello-world.md +++ /dev/null @@ -1,160 +0,0 @@ -# Hello World - -Let's work through the prototypical "Build a todo app" example to showcase how we might build a simple component from scratch. - -## Setup - -We'll create a headline, a list of todo items, and a form to add new items. - -~~~rust -struct TodoList { - headline: SharedString, - items: Vec, - submit_form: ClickHandler -} - -struct TodoItem { - text: SharedString, - completed: bool, - delete: ClickHandler -} - -impl TodoList { - pub fn new( - // Here we impl Into - headline: impl Into, - items: Vec, - submit_form: ClickHandler - ) -> Self { - Self { - // and here we call .into() so we can simply pass a string - // when creating the headline. This pattern is used throughout - // outr components - headline: headline.into(), - items: Vec::new(), - submit_form, - } - } -} -~~~ - -All of this is relatively straightforward. - -We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to efficiently handle shared string data across multiple components and threads without the performance overhead of copying strings. - -When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`. - -~~~rust -use gpui::hsla - -impl TodoList { - // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0)) - } -} -~~~ - -Every component needs a render method, and it should return `impl Element`. This basic component will render a 16x16px yellow square on the screen. - -A couple of questions might come to mind: - -**Why is `size_4()` 16px, not 4px?** - -gpui's style system is based on conventions created by [Tailwind CSS](https://tailwindcss.com/). Here is an example of the list of sizes for `width`: [Width - TailwindCSS Docs](https://tailwindcss.com/docs/width). - -I'll quote from the Tailwind [Core Concepts](https://tailwindcss.com/docs/utility-first) docs here: - -> Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” -> and you’re right, it’s kind of ugly. In fact it’s just about impossible to -> think this is a good idea the first time you see it — -> you have to actually try it. - -As you start using the Tailwind-style conventions you will be surprised how quick it makes it to build out UIs. - -**Why `50.0/360.0` in `hsla()`?** - -gpui [gpui::Hsla] use `0.0-1.0` for all its values, but it is common for tools to use `0-360` for hue. - -This may change in the future, but this is a little trick that let's you use familiar looking values. - -## Building out the container - -Let's grab our [theme::colors::ThemeColors] from the theme and start building out a basic container. - -We can access the current theme's colors like this: - -~~~rust -impl TodoList { - // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let color = cx.theme().colors() - - div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0) - } -} -~~~ - -Now we have access to the complete set of colors defined in the theme. - -~~~rust -use gpui::hsla - -impl TodoList { - // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let color = cx.theme().colors() - - div().size_4().bg(color.surface) - } -} -~~~ - -Let's finish up some basic styles for the container then move on to adding the other elements. - -~~~rust -use gpui::hsla - -impl TodoList { - // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let color = cx.theme().colors() - - div() - // Flex properties - .flex() - .flex_col() // Stack elements vertically - .gap_2() // Add 8px of space between elements - // Size properties - .w_96() // Set width to 384px - .p_4() // Add 16px of padding on all sides - // Color properties - .bg(color.surface) // Set background color - .text_color(color.text) // Set text color - // Border properties - .rounded_md() // Add 4px of border radius - .border_1() // Add a 1px border - .border_color(color.border) - .child( - "Hello, world!" - ) - } -} -~~~ - -### Headline - -TODO - -### List of todo items - -TODO - -### Input - -TODO - - -### End result - -TODO diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 184d841bb1caf0..4ad58998379a82 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -6,6 +6,7 @@ mod disclosure; mod divider; mod dropdown_menu; mod facepile; +mod group; mod icon; mod image; mod indent_guides; @@ -42,6 +43,7 @@ pub use disclosure::*; pub use divider::*; pub use dropdown_menu::*; pub use facepile::*; +pub use group::*; pub use icon::*; pub use image::*; pub use indent_guides::*; diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index 6c2d88916e7fe4..b0670d04c056de 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -1,7 +1,302 @@ -mod avatar; -mod avatar_audio_status_indicator; -mod avatar_availability_indicator; +use crate::prelude::*; -pub use avatar::*; -pub use avatar_audio_status_indicator::*; -pub use avatar_availability_indicator::*; +use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; + +/// An element that renders a user avatar with customizable appearance options. +/// +/// # Examples +/// +/// ``` +/// use ui::{Avatar, AvatarShape}; +/// +/// Avatar::new("path/to/image.png") +/// .shape(AvatarShape::Circle) +/// .grayscale(true) +/// .border_color(gpui::red()); +/// ``` +#[derive(IntoElement, IntoComponent)] +pub struct Avatar { + image: Img, + size: Option, + border_color: Option, + indicator: Option, +} + +impl Avatar { + /// Creates a new avatar element with the specified image source. + pub fn new(src: impl Into) -> Self { + Avatar { + image: img(src), + size: None, + border_color: None, + indicator: None, + } + } + + /// Applies a grayscale filter to the avatar image. + /// + /// # Examples + /// + /// ``` + /// use ui::{Avatar, AvatarShape}; + /// + /// let avatar = Avatar::new("path/to/image.png").grayscale(true); + /// ``` + pub fn grayscale(mut self, grayscale: bool) -> Self { + self.image = self.image.grayscale(grayscale); + self + } + + /// Sets the border color of the avatar. + /// + /// This might be used to match the border to the background color of + /// the parent element to create the illusion of cropping another + /// shape underneath (for example in face piles.) + pub fn border_color(mut self, color: impl Into) -> Self { + self.border_color = Some(color.into()); + self + } + + /// Size overrides the avatar size. By default they are 1rem. + pub fn size>(mut self, size: impl Into>) -> Self { + self.size = size.into().map(Into::into); + self + } + + /// Sets the current indicator to be displayed on the avatar, if any. + pub fn indicator(mut self, indicator: impl Into>) -> Self { + self.indicator = indicator.into().map(IntoElement::into_any_element); + self + } +} + +impl RenderOnce for Avatar { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let border_width = if self.border_color.is_some() { + px(2.) + } else { + px(0.) + }; + + let image_size = self.size.unwrap_or_else(|| rems(1.).into()); + let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; + + div() + .size(container_size) + .rounded_full() + .when_some(self.border_color, |this, color| { + this.border(border_width).border_color(color) + }) + .child( + self.image + .size(image_size) + .rounded_full() + .bg(cx.theme().colors().ghost_element_background), + ) + .children(self.indicator.map(|indicator| div().child(indicator))) + } +} + +use gpui::AnyView; + +/// The audio status of an player, for use in representing +/// their status visually on their avatar. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum AudioStatus { + /// The player's microphone is muted. + Muted, + /// The player's microphone is muted, and collaboration audio is disabled. + Deafened, +} + +/// An indicator that shows the audio status of a player. +#[derive(IntoElement)] +pub struct AvatarAudioStatusIndicator { + audio_status: AudioStatus, + tooltip: Option AnyView>>, +} + +impl AvatarAudioStatusIndicator { + /// Creates a new `AvatarAudioStatusIndicator` + pub fn new(audio_status: AudioStatus) -> Self { + Self { + audio_status, + tooltip: None, + } + } + + /// Sets the tooltip for the indicator. + pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { + self.tooltip = Some(Box::new(tooltip)); + self + } +} + +impl RenderOnce for AvatarAudioStatusIndicator { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let icon_size = IconSize::Indicator; + + let width_in_px = icon_size.rems() * window.rem_size(); + let padding_x = px(4.); + + div() + .absolute() + .bottom(rems_from_px(-3.)) + .right(rems_from_px(-6.)) + .w(width_in_px + padding_x) + .h(icon_size.rems()) + .child( + h_flex() + .id("muted-indicator") + .justify_center() + .px(padding_x) + .py(px(2.)) + .bg(cx.theme().status().error_background) + .rounded_md() + .child( + Icon::new(match self.audio_status { + AudioStatus::Muted => IconName::MicMute, + AudioStatus::Deafened => IconName::AudioOff, + }) + .size(icon_size) + .color(Color::Error), + ) + .when_some(self.tooltip, |this, tooltip| { + this.tooltip(move |window, cx| tooltip(window, cx)) + }), + ) + } +} + +/// Represents the availability status of a collaborator. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum CollaboratorAvailability { + Free, + Busy, +} + +/// Represents the availability and presence status of a collaborator. +#[derive(IntoElement)] +pub struct AvatarAvailabilityIndicator { + availability: CollaboratorAvailability, + avatar_size: Option, +} + +impl AvatarAvailabilityIndicator { + /// Creates a new indicator + pub fn new(availability: CollaboratorAvailability) -> Self { + Self { + availability, + avatar_size: None, + } + } + + /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on. + pub fn avatar_size(mut self, size: impl Into>) -> Self { + self.avatar_size = size.into(); + self + } +} + +impl RenderOnce for AvatarAvailabilityIndicator { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size()); + + // HACK: non-integer sizes result in oval indicators. + let indicator_size = (avatar_size * 0.4).round(); + + div() + .absolute() + .bottom_0() + .right_0() + .size(indicator_size) + .rounded(indicator_size) + .bg(match self.availability { + CollaboratorAvailability::Free => cx.theme().status().created, + CollaboratorAvailability::Busy => cx.theme().status().deleted, + }) + } +} + +// View this component preview using `workspace: open component-preview` +impl ComponentPreview for Avatar { + fn preview(_window: &mut Window, cx: &App) -> AnyElement { + let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; + + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Sizes", + vec![ + single_example("Default", Avatar::new(example_avatar).into_any_element()), + single_example( + "Small", + Avatar::new(example_avatar).size(px(24.)).into_any_element(), + ), + single_example( + "Large", + Avatar::new(example_avatar).size(px(48.)).into_any_element(), + ), + ], + ), + example_group_with_title( + "Styles", + vec![ + single_example("Default", Avatar::new(example_avatar).into_any_element()), + single_example( + "Grayscale", + Avatar::new(example_avatar) + .grayscale(true) + .into_any_element(), + ), + single_example( + "With Border", + Avatar::new(example_avatar) + .border_color(cx.theme().colors().border) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Audio Status", + vec![ + single_example( + "Muted", + Avatar::new(example_avatar) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)) + .into_any_element(), + ), + single_example( + "Deafened", + Avatar::new(example_avatar) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Availability", + vec![ + single_example( + "Free", + Avatar::new(example_avatar) + .indicator(AvatarAvailabilityIndicator::new( + CollaboratorAvailability::Free, + )) + .into_any_element(), + ), + single_example( + "Busy", + Avatar::new(example_avatar) + .indicator(AvatarAvailabilityIndicator::new( + CollaboratorAvailability::Busy, + )) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs deleted file mode 100644 index 65622f8c3f2ffd..00000000000000 --- a/crates/ui/src/components/avatar/avatar.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::{prelude::*, Indicator}; - -use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; - -/// An element that renders a user avatar with customizable appearance options. -/// -/// # Examples -/// -/// ``` -/// use ui::{Avatar, AvatarShape}; -/// -/// Avatar::new("path/to/image.png") -/// .shape(AvatarShape::Circle) -/// .grayscale(true) -/// .border_color(gpui::red()); -/// ``` -#[derive(IntoElement, IntoComponent)] -pub struct Avatar { - image: Img, - size: Option, - border_color: Option, - indicator: Option, -} - -impl Avatar { - /// Creates a new avatar element with the specified image source. - pub fn new(src: impl Into) -> Self { - Avatar { - image: img(src), - size: None, - border_color: None, - indicator: None, - } - } - - /// Applies a grayscale filter to the avatar image. - /// - /// # Examples - /// - /// ``` - /// use ui::{Avatar, AvatarShape}; - /// - /// let avatar = Avatar::new("path/to/image.png").grayscale(true); - /// ``` - pub fn grayscale(mut self, grayscale: bool) -> Self { - self.image = self.image.grayscale(grayscale); - self - } - - /// Sets the border color of the avatar. - /// - /// This might be used to match the border to the background color of - /// the parent element to create the illusion of cropping another - /// shape underneath (for example in face piles.) - pub fn border_color(mut self, color: impl Into) -> Self { - self.border_color = Some(color.into()); - self - } - - /// Size overrides the avatar size. By default they are 1rem. - pub fn size>(mut self, size: impl Into>) -> Self { - self.size = size.into().map(Into::into); - self - } - - /// Sets the current indicator to be displayed on the avatar, if any. - pub fn indicator(mut self, indicator: impl Into>) -> Self { - self.indicator = indicator.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for Avatar { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let border_width = if self.border_color.is_some() { - px(2.) - } else { - px(0.) - }; - - let image_size = self.size.unwrap_or_else(|| rems(1.).into()); - let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; - - div() - .size(container_size) - .rounded_full() - .when_some(self.border_color, |this, color| { - this.border(border_width).border_color(color) - }) - .child( - self.image - .size(image_size) - .rounded_full() - .bg(cx.theme().colors().ghost_element_background), - ) - .children(self.indicator.map(|indicator| div().child(indicator))) - } -} - -// View this component preview using `workspace: open component-preview` -impl ComponentPreview for Avatar { - fn preview(_window: &mut Window, _cx: &App) -> AnyElement { - let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; - - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![ - single_example( - "Default", - Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4") - .into_any_element(), - ), - single_example( - "Small", - Avatar::new(example_avatar).size(px(24.)).into_any_element(), - ), - single_example( - "Large", - Avatar::new(example_avatar).size(px(48.)).into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example("Default", Avatar::new(example_avatar).into_any_element()), - single_example( - "Grayscale", - Avatar::new(example_avatar) - .grayscale(true) - .into_any_element(), - ), - single_example( - "With Border", - Avatar::new(example_avatar) - .border_color(gpui::red()) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Indicator", - vec![single_example( - "Dot", - Avatar::new(example_avatar) - .indicator(Indicator::dot().color(Color::Success)) - .into_any_element(), - )], - ), - ]) - .into_any_element() - } -} diff --git a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs deleted file mode 100644 index 23e093880b232b..00000000000000 --- a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs +++ /dev/null @@ -1,72 +0,0 @@ -use gpui::AnyView; - -use crate::prelude::*; - -/// The audio status of an player, for use in representing -/// their status visually on their avatar. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum AudioStatus { - /// The player's microphone is muted. - Muted, - /// The player's microphone is muted, and collaboration audio is disabled. - Deafened, -} - -/// An indicator that shows the audio status of a player. -#[derive(IntoElement)] -pub struct AvatarAudioStatusIndicator { - audio_status: AudioStatus, - tooltip: Option AnyView>>, -} - -impl AvatarAudioStatusIndicator { - /// Creates a new `AvatarAudioStatusIndicator` - pub fn new(audio_status: AudioStatus) -> Self { - Self { - audio_status, - tooltip: None, - } - } - - /// Sets the tooltip for the indicator. - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } -} - -impl RenderOnce for AvatarAudioStatusIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_size = IconSize::Indicator; - - let width_in_px = icon_size.rems() * window.rem_size(); - let padding_x = px(4.); - - div() - .absolute() - .bottom(rems_from_px(-3.)) - .right(rems_from_px(-6.)) - .w(width_in_px + padding_x) - .h(icon_size.rems()) - .child( - h_flex() - .id("muted-indicator") - .justify_center() - .px(padding_x) - .py(px(2.)) - .bg(cx.theme().status().error_background) - .rounded_md() - .child( - Icon::new(match self.audio_status { - AudioStatus::Muted => IconName::MicMute, - AudioStatus::Deafened => IconName::AudioOff, - }) - .size(icon_size) - .color(Color::Error), - ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }), - ) - } -} diff --git a/crates/ui/src/components/avatar/avatar_availability_indicator.rs b/crates/ui/src/components/avatar/avatar_availability_indicator.rs deleted file mode 100644 index 126e12c91ab78d..00000000000000 --- a/crates/ui/src/components/avatar/avatar_availability_indicator.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![allow(missing_docs)] -use crate::prelude::*; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum Availability { - Free, - Busy, -} - -#[derive(IntoElement)] -pub struct AvatarAvailabilityIndicator { - availability: Availability, - avatar_size: Option, -} - -impl AvatarAvailabilityIndicator { - pub fn new(availability: Availability) -> Self { - Self { - availability, - avatar_size: None, - } - } - - /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on. - pub fn avatar_size(mut self, size: impl Into>) -> Self { - self.avatar_size = size.into(); - self - } -} - -impl RenderOnce for AvatarAvailabilityIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size()); - - // HACK: non-integer sizes result in oval indicators. - let indicator_size = (avatar_size * 0.4).round(); - - div() - .absolute() - .bottom_0() - .right_0() - .size(indicator_size) - .rounded(indicator_size) - .bg(match self.availability { - Availability::Free => cx.theme().status().created, - Availability::Busy => cx.theme().status().deleted, - }) - } -} diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 58a3d5ae9175ee..e7112aa8aec509 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use component::{example_group_with_title, single_example, ComponentPreview}; use gpui::{AnyElement, AnyView, DefiniteLength}; use ui_macros::IntoComponent; @@ -81,6 +80,7 @@ use super::button_icon::ButtonIcon; /// ``` /// #[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct Button { base: ButtonLike, label: SharedString, @@ -463,7 +463,7 @@ impl ComponentPreview for Button { .gap_6() .children(vec![ example_group_with_title( - "Styles", + "Button Styles", vec![ single_example( "Default", @@ -481,6 +481,12 @@ impl ComponentPreview for Button { .style(ButtonStyle::Subtle) .into_any_element(), ), + single_example( + "Tinted", + Button::new("tinted_accent_style", "Accent") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .into_any_element(), + ), single_example( "Transparent", Button::new("transparent", "Transparent") @@ -490,7 +496,7 @@ impl ComponentPreview for Button { ], ), example_group_with_title( - "Tinted", + "Tint Styles", vec![ single_example( "Accent", @@ -519,7 +525,7 @@ impl ComponentPreview for Button { ], ), example_group_with_title( - "States", + "Special States", vec![ single_example( "Default", @@ -540,7 +546,7 @@ impl ComponentPreview for Button { ], ), example_group_with_title( - "With Icons", + "Buttons with Icons", vec![ single_example( "Icon Start", @@ -563,16 +569,6 @@ impl ComponentPreview for Button { .icon_color(Color::Accent) .into_any_element(), ), - single_example( - "Tinted Icons", - Button::new("tinted_icons", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)) - .color(Color::Error) - .icon_color(Color::Error) - .icon(IconName::Trash) - .icon_position(IconPosition::Start) - .into_any_element(), - ), ], ), ]) diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index adacd12f27039f..337f10700a1098 100644 --- a/crates/ui/src/components/button/button_icon.rs +++ b/crates/ui/src/components/button/button_icon.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use crate::{prelude::*, Icon, IconName, IconSize, IconWithIndicator, Indicator}; use gpui::Hsla; diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 96d093c249ab4a..5503e6f865afa8 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use gpui::{relative, CursorStyle, DefiniteLength, MouseButton}; use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems}; use smallvec::SmallVec; diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 204ea8e564c888..97084116057e5e 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -1,8 +1,7 @@ -#![allow(missing_docs)] use gpui::{AnyView, DefiniteLength, Hsla}; use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle}; -use crate::{prelude::*, ElevationIndex, Indicator, SelectableButton}; +use crate::{prelude::*, ElevationIndex, Indicator, SelectableButton, TintColor}; use crate::{IconName, IconSize}; use super::button_icon::ButtonIcon; @@ -14,7 +13,8 @@ pub enum IconButtonShape { Wide, } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct IconButton { base: ButtonLike, shape: IconButtonShape, @@ -200,3 +200,160 @@ impl RenderOnce for IconButton { ) } } + +impl ComponentPreview for IconButton { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Icon Button Styles", + vec![ + single_example( + "Default", + IconButton::new("default", IconName::Check) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "Filled", + IconButton::new("filled", IconName::Check) + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "Subtle", + IconButton::new("subtle", IconName::Check) + .layer(ElevationIndex::Background) + .style(ButtonStyle::Subtle) + .into_any_element(), + ), + single_example( + "Tinted", + IconButton::new("tinted", IconName::Check) + .layer(ElevationIndex::Background) + .style(ButtonStyle::Tinted(TintColor::Accent)) + .into_any_element(), + ), + single_example( + "Transparent", + IconButton::new("transparent", IconName::Check) + .layer(ElevationIndex::Background) + .style(ButtonStyle::Transparent) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Icon Button Shapes", + vec![ + single_example( + "Square", + IconButton::new("square", IconName::Check) + .shape(IconButtonShape::Square) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "Wide", + IconButton::new("wide", IconName::Check) + .shape(IconButtonShape::Wide) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Icon Button Sizes", + vec![ + single_example( + "Small", + IconButton::new("small", IconName::Check) + .icon_size(IconSize::XSmall) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "Small", + IconButton::new("small", IconName::Check) + .icon_size(IconSize::Small) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "Medium", + IconButton::new("medium", IconName::Check) + .icon_size(IconSize::Medium) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "XLarge", + IconButton::new("xlarge", IconName::Check) + .icon_size(IconSize::XLarge) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Special States", + vec![ + single_example( + "Disabled", + IconButton::new("disabled", IconName::Check) + .disabled(true) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "Selected", + IconButton::new("selected", IconName::Check) + .toggle_state(true) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "With Indicator", + IconButton::new("indicator", IconName::Check) + .indicator(Indicator::dot().color(Color::Success)) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Custom Colors", + vec![ + single_example( + "Custom Icon Color", + IconButton::new("custom_color", IconName::Check) + .icon_color(Color::Accent) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + single_example( + "With Alpha", + IconButton::new("alpha", IconName::Check) + .alpha(0.5) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::Background) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 9681e8031bd70b..1fb8a2c01633ff 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use gpui::{AnyView, ClickEvent}; use crate::{prelude::*, ButtonLike, ButtonLikeRounding, ElevationIndex}; @@ -16,7 +15,8 @@ pub enum ToggleButtonPosition { Last, } -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] +#[component(scope = "input")] pub struct ToggleButton { base: ButtonLike, position_in_group: Option, @@ -142,3 +142,130 @@ impl RenderOnce for ToggleButton { ) } } + +impl ComponentPreview for ToggleButton { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Button Styles", + vec![ + single_example( + "Off", + ToggleButton::new("off", "Off") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "On", + ToggleButton::new("on", "On") + .layer(ElevationIndex::Background) + .toggle_state(true) + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "Off – Disabled", + ToggleButton::new("disabled_off", "Disabled Off") + .layer(ElevationIndex::Background) + .disabled(true) + .style(ButtonStyle::Filled) + .into_any_element(), + ), + single_example( + "On – Disabled", + ToggleButton::new("disabled_on", "Disabled On") + .layer(ElevationIndex::Background) + .disabled(true) + .toggle_state(true) + .style(ButtonStyle::Filled) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Button Group", + vec![ + single_example( + "Three Buttons", + h_flex() + .child( + ToggleButton::new("three_btn_first", "First") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .first() + .into_any_element(), + ) + .child( + ToggleButton::new("three_btn_middle", "Middle") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .middle() + .toggle_state(true) + .into_any_element(), + ) + .child( + ToggleButton::new("three_btn_last", "Last") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .last() + .into_any_element(), + ) + .into_any_element(), + ), + single_example( + "Two Buttons", + h_flex() + .child( + ToggleButton::new("two_btn_first", "First") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .first() + .into_any_element(), + ) + .child( + ToggleButton::new("two_btn_last", "Last") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .last() + .into_any_element(), + ) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Alternate Sizes", + vec![ + single_example( + "None", + ToggleButton::new("none", "None") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .size(ButtonSize::None) + .into_any_element(), + ), + single_example( + "Compact", + ToggleButton::new("compact", "Compact") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .size(ButtonSize::Compact) + .into_any_element(), + ), + single_example( + "Large", + ToggleButton::new("large", "Large") + .layer(ElevationIndex::Background) + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs index 30c115b2a557ac..e372580745931d 100644 --- a/crates/ui/src/components/content_group.rs +++ b/crates/ui/src/components/content_group.rs @@ -11,14 +11,14 @@ pub fn content_group() -> ContentGroup { /// A [ContentGroup] that vertically stacks its children. /// /// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex). -pub fn v_group() -> ContentGroup { +pub fn v_container() -> ContentGroup { content_group().v_flex() } /// Creates a new horizontal [ContentGroup]. /// /// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex). -pub fn h_group() -> ContentGroup { +pub fn h_container() -> ContentGroup { content_group().h_flex() } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 03e5fa407db791..0d3eb8b2a4b9f5 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use crate::{ h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, @@ -507,7 +506,7 @@ impl ContextMenuItem { impl Render for ContextMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let aside = self .documentation_aside diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 036a26090e0721..c39bcf47d50b97 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use std::sync::Arc; use gpui::{ClickEvent, CursorStyle}; diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 2a0f87a610c73d..6c539704237568 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -1,8 +1,25 @@ -#![allow(missing_docs)] use gpui::{Hsla, IntoElement}; use crate::prelude::*; +pub fn divider() -> Divider { + Divider { + style: DividerStyle::Solid, + direction: DividerDirection::Horizontal, + color: DividerColor::default(), + inset: false, + } +} + +pub fn vertical_divider() -> Divider { + Divider { + style: DividerStyle::Solid, + direction: DividerDirection::Vertical, + color: DividerColor::default(), + inset: false, + } +} + #[derive(Clone, Copy, PartialEq)] enum DividerStyle { Solid, diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index 1f2e0473af4fe1..7d2072d6c9daae 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton}; use crate::{prelude::*, ContextMenu, PopoverMenu}; diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index d965bc598a457b..59df3f4c005c9a 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{prelude::*, Avatar}; use gpui::{AnyElement, StyleRefinement}; use smallvec::SmallVec; @@ -7,7 +7,7 @@ use smallvec::SmallVec; /// /// Facepiles are used to display a group of people or things, /// such as a list of participants in a collaboration session. -#[derive(IntoElement)] +#[derive(IntoElement, IntoComponent)] pub struct Facepile { base: Div, faces: SmallVec<[AnyElement; 2]>, @@ -60,60 +60,57 @@ impl RenderOnce for Facepile { } } -// impl ComponentPreview for Facepile { -// fn description() -> impl Into> { -// "A facepile is a collection of faces stacked horizontally–\ -// always with the leftmost face on top and descending in z-index.\ -// \n\nFacepiles are used to display a group of people or things,\ -// such as a list of participants in a collaboration session." -// } -// fn examples(_window: &mut Window, _: &mut App) -> 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", -// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", -// ]; +impl ComponentPreview for Facepile { + fn preview(_window: &mut Window, _cx: &App) -> AnyElement { + let faces: [&'static str; 6] = [ + "https://avatars.githubusercontent.com/u/326587?s=60&v=4", + "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", + "https://avatars.githubusercontent.com/u/1789?s=60&v=4", + "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", + "https://avatars.githubusercontent.com/u/482957?s=60&v=4", + "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", + ]; -// let many_faces: [&'static str; 6] = [ -// "https://avatars.githubusercontent.com/u/326587?s=60&v=4", -// "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", -// "https://avatars.githubusercontent.com/u/1789?s=60&v=4", -// "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", -// "https://avatars.githubusercontent.com/u/482957?s=60&v=4", -// "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", -// ]; - -// vec![example_group_with_title( -// "Examples", -// vec![ -// single_example( -// "Few Faces", -// Facepile::new( -// few_faces -// .iter() -// .map(|&url| Avatar::new(url).into_any_element()) -// .collect(), -// ), -// ), -// single_example( -// "Many Faces", -// Facepile::new( -// many_faces -// .iter() -// .map(|&url| Avatar::new(url).into_any_element()) -// .collect(), -// ), -// ), -// single_example( -// "Custom Size", -// Facepile::new( -// few_faces -// .iter() -// .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) -// .collect(), -// ), -// ), -// ], -// )] -// } -// } + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Facepile Examples", + vec![ + single_example( + "Default", + Facepile::new( + faces + .iter() + .map(|&url| Avatar::new(url).into_any_element()) + .collect(), + ) + .into_any_element(), + ), + single_example( + "Custom Size", + Facepile::new( + faces + .iter() + .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) + .collect(), + ) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Special Cases", + vec![ + single_example("Empty Facepile", Facepile::empty().into_any_element()), + single_example( + "Single Face", + Facepile::new(vec![Avatar::new(faces[0]).into_any_element()].into()) + .into_any_element(), + ), + ], + ), + ]) + .into_any_element() + } +} diff --git a/crates/ui/src/components/group.rs b/crates/ui/src/components/group.rs new file mode 100644 index 00000000000000..f49ab9fe1377ea --- /dev/null +++ b/crates/ui/src/components/group.rs @@ -0,0 +1,57 @@ +use gpui::{div, prelude::*, Div}; + +/// Creates a horizontal group with tight, consistent spacing. +/// +/// xs: ~2px @16px/rem +pub fn h_group_sm() -> Div { + div().flex().gap_0p5() +} + +/// Creates a horizontal group with consistent spacing. +/// +/// s: ~4px @16px/rem +pub fn h_group() -> Div { + div().flex().gap_1() +} + +/// Creates a horizontal group with consistent spacing. +/// +/// m: ~6px @16px/rem +pub fn h_group_lg() -> Div { + div().flex().gap_1p5() +} + +/// Creates a horizontal group with consistent spacing. +/// +/// l: ~8px @16px/rem +pub fn h_group_xl() -> Div { + div().flex().gap_2() +} + +/// Creates a vertical group with tight, consistent spacing. +/// +/// xs: ~2px @16px/rem +pub fn v_group_sm() -> Div { + div().flex().flex_col().gap_0p5() +} + +/// Creates a vertical group with consistent spacing. +/// +/// s: ~4px @16px/rem +pub fn v_group() -> Div { + div().flex().flex_col().gap_1() +} + +/// Creates a vertical group with consistent spacing. +/// +/// m: ~6px @16px/rem +pub fn v_group_lg() -> Div { + div().flex().flex_col().gap_1p5() +} + +/// Creates a vertical group with consistent spacing. +/// +/// l: ~8px @16px/rem +pub fn v_group_xl() -> Div { + div().flex().flex_col().gap_2() +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index ca90a16ea7029c..ddb03071262458 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - mod decorated_icon; mod icon_decoration; diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index cb90b9a1e388fe..cee5cb3538ec7e 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use gpui::{svg, App, IntoElement, Rems, RenderOnce, Size, Styled, Window}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 87c390ef77d2e7..0d9c3f5571c3a9 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use std::{cmp::Ordering, ops::Range, rc::Rc}; use gpui::{ diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index 0cf4cab72eba99..86d1f31e409382 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use crate::{prelude::*, AnyIcon}; #[derive(Default)] diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index c3a3e6579d4961..13fb412e3b8f77 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use crate::PlatformStyle; use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use gpui::{ diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index a8be4624039eed..df6866aee8fad0 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::ops::Range; use gpui::{FontWeight, HighlightStyle, StyledText}; diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs index 88f9c54246b19e..7cd81a51648180 100644 --- a/crates/ui/src/components/list/list.rs +++ b/crates/ui/src/components/list/list.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs index d7a5c7bbaf4f58..91bb0ca76a2a23 100644 --- a/crates/ui/src/components/list/list_header.rs +++ b/crates/ui/src/components/list/list_header.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::sync::Arc; use crate::{h_flex, prelude::*, Disclosure, Label}; diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 7eec79cfd74c46..2d2e506e62d4e3 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::sync::Arc; use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels}; diff --git a/crates/ui/src/components/list/list_separator.rs b/crates/ui/src/components/list/list_separator.rs index f3a900404f44b3..92a7c987c76f7d 100644 --- a/crates/ui/src/components/list/list_separator.rs +++ b/crates/ui/src/components/list/list_separator.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use crate::prelude::*; #[derive(IntoElement)] diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index 297068fc85b371..3cf2fd51d7d71c 100644 --- a/crates/ui/src/components/list/list_sub_header.rs +++ b/crates/ui/src/components/list/list_sub_header.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use crate::prelude::*; use crate::{h_flex, Icon, IconName, IconSize, Label}; diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 601c4228c6ddac..b148e2902b2984 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use crate::{ h_flex, v_flex, Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize, diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 87cdfbee645092..d491732b85a0f8 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use gpui::ClickEvent; use crate::{prelude::*, IconButtonShape}; diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index 85033eb32f10cc..2ac0b8f5fc9f6b 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use crate::prelude::*; use crate::v_flex; use gpui::{ diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index e3ce37b43e0b74..6be332b6932e1b 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::{cell::RefCell, rc::Rc}; use gpui::{ diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs index d7ee106d2d5bb0..c7e19f5c34701e 100644 --- a/crates/ui/src/components/radio.rs +++ b/crates/ui/src/components/radio.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::sync::Arc; use crate::prelude::*; diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 1bbf5aeedd292b..9d171e6daa26ee 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use std::{cell::RefCell, rc::Rc}; use gpui::{ diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index a9793adea81e23..155eaed8a9fd92 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc}; use crate::{prelude::*, px, relative, IntoElement}; @@ -99,7 +98,7 @@ pub trait ScrollableHandle: Debug + 'static { #[derive(Clone, Debug)] pub struct ScrollbarState { // If Some(), there's an active drag, offset by percentage from the origin of a thumb. - drag: Rc>>, + drag: Rc>>, parent_id: Option, scroll_handle: Arc, } @@ -128,12 +127,12 @@ impl ScrollbarState { } fn thumb_range(&self, axis: ScrollbarAxis) -> Option> { - const MINIMUM_SCROLLBAR_PERCENTAGE_SIZE: f32 = 0.005; + const MINIMUM_THUMB_SIZE: f32 = 25.; let ContentSize { size: main_dimension_size, scroll_adjustment, } = self.scroll_handle.content_size()?; - let main_dimension_size = main_dimension_size.along(axis).0; + let content_size = main_dimension_size.along(axis).0; let mut current_offset = self.scroll_handle.offset().along(axis).min(px(0.)).abs().0; if let Some(adjustment) = scroll_adjustment.and_then(|adjustment| { let adjust = adjustment.along(axis).0; @@ -145,25 +144,21 @@ impl ScrollbarState { }) { current_offset -= adjustment; } - - let mut percentage = current_offset / main_dimension_size; - let viewport_size = self.scroll_handle.viewport().size; - let end_offset = (current_offset + viewport_size.along(axis).0) / main_dimension_size; - // Scroll handle might briefly report an offset greater than the length of a list; - // in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable. - let overshoot = (end_offset - 1.).clamp(0., 1.); - if overshoot > 0. { - percentage -= overshoot; - } - if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_SIZE > 1.0 || end_offset > main_dimension_size - { + let viewport_size = self.scroll_handle.viewport().size.along(axis).0; + if content_size < viewport_size { return None; } - if main_dimension_size < viewport_size.along(axis).0 { + let visible_percentage = viewport_size / content_size; + let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); + if thumb_size > viewport_size { return None; } - let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_SIZE, 1.); - Some(percentage..end_offset) + let max_offset = content_size - viewport_size; + current_offset = current_offset.clamp(0., max_offset); + let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size); + let thumb_percentage_start = start_offset / viewport_size; + let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; + Some(thumb_percentage_start..thumb_percentage_end) } } @@ -294,43 +289,47 @@ impl Element for Scrollbar { )); let scroll = self.state.scroll_handle.clone(); - let kind = self.kind; - let thumb_percentage_size = self.thumb.end - self.thumb.start; + let axis = self.kind; window.on_mouse_event({ let scroll = scroll.clone(); let state = self.state.clone(); - let axis = self.kind; move |event: &MouseDownEvent, phase, _, _| { if !(phase.bubble() && bounds.contains(&event.position)) { return; } if thumb_bounds.contains(&event.position) { - let thumb_offset = (event.position.along(axis) - - thumb_bounds.origin.along(axis)) - / bounds.size.along(axis); - state.drag.set(Some(thumb_offset)); + let offset = event.position.along(axis) - thumb_bounds.origin.along(axis); + state.drag.set(Some(offset)); } else if let Some(ContentSize { size: item_size, .. }) = scroll.content_size() { - match kind { + let click_offset = { + let viewport_size = padded_bounds.size.along(axis); + + let thumb_size = thumb_bounds.size.along(axis); + let thumb_start = (event.position.along(axis) + - padded_bounds.origin.along(axis) + - (thumb_size / 2.)) + .clamp(px(0.), viewport_size - thumb_size); + + let max_offset = (item_size.along(axis) - viewport_size).max(px(0.)); + let percentage = if viewport_size > thumb_size { + thumb_start / (viewport_size - thumb_size) + } else { + 0. + }; + + -max_offset * percentage + }; + match axis { ScrollbarAxis::Horizontal => { - let percentage = - (event.position.x - bounds.origin.x) / bounds.size.width; - let max_offset = item_size.width; - let percentage = percentage.min(1. - thumb_percentage_size); - scroll - .set_offset(point(-max_offset * percentage, scroll.offset().y)); + scroll.set_offset(point(click_offset, scroll.offset().y)); } ScrollbarAxis::Vertical => { - let percentage = - (event.position.y - bounds.origin.y) / bounds.size.height; - let max_offset = item_size.height; - let percentage = percentage.min(1. - thumb_percentage_size); - scroll - .set_offset(point(scroll.offset().x, -max_offset * percentage)); + scroll.set_offset(point(scroll.offset().x, click_offset)); } } } @@ -348,36 +347,39 @@ impl Element for Scrollbar { } }); let state = self.state.clone(); - let kind = self.kind; + let axis = self.kind; window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| { if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) { if let Some(ContentSize { size: item_size, .. }) = scroll.content_size() { - match kind { + let drag_offset = { + let viewport_size = padded_bounds.size.along(axis); + + let thumb_size = thumb_bounds.size.along(axis); + let thumb_start = (event.position.along(axis) + - padded_bounds.origin.along(axis) + - drag_state) + .clamp(px(0.), viewport_size - thumb_size); + + let max_offset = (item_size.along(axis) - viewport_size).max(px(0.)); + let percentage = if viewport_size > thumb_size { + thumb_start / (viewport_size - thumb_size) + } else { + 0. + }; + + -max_offset * percentage + }; + match axis { ScrollbarAxis::Horizontal => { - let max_offset = item_size.width; - let percentage = (event.position.x - bounds.origin.x) - / bounds.size.width - - drag_state; - - let percentage = percentage.min(1. - thumb_percentage_size); - scroll - .set_offset(point(-max_offset * percentage, scroll.offset().y)); + scroll.set_offset(point(drag_offset, scroll.offset().y)); } ScrollbarAxis::Vertical => { - let max_offset = item_size.height; - let percentage = (event.position.y - bounds.origin.y) - / bounds.size.height - - drag_state; - - let percentage = percentage.min(1. - thumb_percentage_size); - scroll - .set_offset(point(scroll.offset().x, -max_offset * percentage)); + scroll.set_offset(point(scroll.offset().x, drag_offset)); } }; - if let Some(id) = state.parent_id { cx.notify(id); } diff --git a/crates/ui/src/components/settings_container.rs b/crates/ui/src/components/settings_container.rs index 538a5b91f849e3..b8a4a021c623fb 100644 --- a/crates/ui/src/components/settings_container.rs +++ b/crates/ui/src/components/settings_container.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/settings_group.rs b/crates/ui/src/components/settings_group.rs index 9ead450179096f..90cd1b16276ee1 100644 --- a/crates/ui/src/components/settings_group.rs +++ b/crates/ui/src/components/settings_group.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs index 2af0a5d3f98b08..74a5e80575bfe0 100644 --- a/crates/ui/src/components/stack.rs +++ b/crates/ui/src/components/stack.rs @@ -1,5 +1,3 @@ -#![allow(missing_docs)] - use gpui::{div, Div}; use crate::StyledExt; diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs index 9fa380c70319a8..69bd5715861846 100644 --- a/crates/ui/src/components/stories.rs +++ b/crates/ui/src/components/stories.rs @@ -1,8 +1,3 @@ -// We allow missing docs for stories as the docs will more or less be -// "This is the ___ story", which is not very useful. -#![allow(missing_docs)] -mod avatar; -mod button; mod context_menu; mod disclosure; mod icon; @@ -15,8 +10,6 @@ mod tab; mod tab_bar; mod toggle_button; -pub use avatar::*; -pub use button::*; pub use context_menu::*; pub use disclosure::*; pub use icon::*; diff --git a/crates/ui/src/components/stories/avatar.rs b/crates/ui/src/components/stories/avatar.rs index 8ad2aca8aff170..e69de29bb2d1d6 100644 --- a/crates/ui/src/components/stories/avatar.rs +++ b/crates/ui/src/components/stories/avatar.rs @@ -1,64 +0,0 @@ -use gpui::Render; -use story::{Story, StoryItem, StorySection}; - -use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator}; -use crate::{Avatar, AvatarAudioStatusIndicator}; - -pub struct AvatarStory; - -impl Render for AvatarStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container() - .child(Story::title_for::()) - .child( - StorySection::new() - .child(StoryItem::new( - "Default", - Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4"), - )) - .child(StoryItem::new( - "Default", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4"), - )), - ) - .child( - StorySection::new() - .child(StoryItem::new( - "With free availability indicator", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), - )) - .child(StoryItem::new( - "With busy availability indicator", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)), - )), - ) - .child( - StorySection::new() - .child(StoryItem::new( - "With info border", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .border_color(cx.theme().status().info_border), - )) - .child(StoryItem::new( - "With error border", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .border_color(cx.theme().status().error_border), - )), - ) - .child( - StorySection::new() - .child(StoryItem::new( - "With muted audio indicator", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)), - )) - .child(StoryItem::new( - "With deafened audio indicator", - Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4") - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)), - )), - ) - } -} diff --git a/crates/ui/src/components/stories/button.rs b/crates/ui/src/components/stories/button.rs index 730a15471b5092..e69de29bb2d1d6 100644 --- a/crates/ui/src/components/stories/button.rs +++ b/crates/ui/src/components/stories/button.rs @@ -1,38 +0,0 @@ -use gpui::Render; -use story::Story; - -use crate::{prelude::*, IconName}; -use crate::{Button, ButtonStyle}; - -pub struct ButtonStory; - -impl Render for ButtonStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - Story::container() - .child(Story::title_for::