From 6b9397c380280899573f62ea5e4c140842eaee67 Mon Sep 17 00:00:00 2001 From: 0x2CA <2478557459@qq.com> Date: Fri, 21 Feb 2025 14:39:11 +0800 Subject: [PATCH 01/17] vim: Fix `gr` in visual mode (#25301) Closes #25258 Release Notes: - Fixed `gr` in visual mode --- assets/keymaps/vim.json | 1 + crates/vim/src/normal/paste.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) 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/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 849fa968c87ef1..ea0e57315fcdfd 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -849,6 +849,26 @@ mod test { ); let clipboard: Register = cx.read_from_clipboard().unwrap().into(); assert_eq!(clipboard.text, "fish"); + + cx.set_state( + indoc! {" + ˇfish one + two three + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("y i w"); + cx.simulate_keystrokes("w"); + cx.simulate_keystrokes("v i w g r"); + cx.assert_state( + indoc! {" + fish fisˇh + two three + "}, + Mode::Normal, + ); + let clipboard: Register = cx.read_from_clipboard().unwrap().into(); + assert_eq!(clipboard.text, "fish"); } #[gpui::test] From 4871d3c9e761eee43645283fb319162a846f8ff9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Feb 2025 23:52:34 -0700 Subject: [PATCH 02/17] New commit review flow in project diff view (#25229) Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Nate Butler --- Cargo.lock | 14 +- assets/keymaps/default-macos.json | 8 + crates/editor/src/editor.rs | 47 +++- crates/editor/src/element.rs | 204 +++++++++----- crates/git_ui/src/commit_modal.rs | 244 +++++++++++++++++ crates/git_ui/src/git_panel.rs | 208 +++++++++------ crates/git_ui/src/git_ui.rs | 4 +- crates/git_ui/src/project_diff.rs | 305 ++++++++++++++++++++- crates/git_ui/src/quick_commit.rs | 307 ---------------------- crates/ui/src/components.rs | 2 + crates/ui/src/components/content_group.rs | 4 +- crates/ui/src/components/divider.rs | 18 ++ crates/ui/src/components/group.rs | 57 ++++ crates/ui/src/components/tooltip.rs | 18 ++ crates/ui/src/prelude.rs | 5 +- crates/welcome/src/welcome.rs | 2 +- crates/workspace/src/dock.rs | 6 +- crates/zed/src/zed.rs | 3 + 18 files changed, 979 insertions(+), 477 deletions(-) create mode 100644 crates/git_ui/src/commit_modal.rs create mode 100644 crates/ui/src/components/group.rs 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/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/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/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs new file mode 100644 index 00000000000000..e61b8faa4a37bc --- /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.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/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/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/divider.rs b/crates/ui/src/components/divider.rs index 2a0f87a610c73d..c6e31ee7deb569 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -3,6 +3,24 @@ 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/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/tooltip.rs b/crates/ui/src/components/tooltip.rs index cd391bb7f1a0ab..e88f80edeab6f6 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -52,6 +52,24 @@ impl Tooltip { } } + pub fn for_action_title_in( + title: impl Into, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> impl Fn(&mut Window, &mut App) -> AnyView { + let title = title.into(); + let action = action.boxed_clone(); + let focus_handle = focus_handle.clone(); + move |window, cx| { + cx.new(|cx| Self { + title: title.clone(), + meta: None, + key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx), + }) + .into() + } + } + pub fn for_action( title: impl Into, action: &dyn Action, diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index cecb91ea43381b..0b9ce91f1e9f15 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -18,7 +18,10 @@ pub use crate::traits::styled_ext::*; pub use crate::traits::toggleable::*; pub use crate::traits::visible_on_hover::*; pub use crate::DynamicSpacing; -pub use crate::{h_flex, h_group, v_flex, v_group}; +pub use crate::{h_container, h_flex, v_container, v_flex}; +pub use crate::{ + h_group, h_group_lg, h_group_sm, h_group_xl, v_group, v_group_lg, v_group_sm, v_group_xl, +}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color}; pub use crate::{Headline, HeadlineSize}; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 5e8e4ff314ae0e..c95dccb2f7820f 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -265,7 +265,7 @@ impl Render for WelcomePage { ), ) .child( - v_group() + v_container() .gap_2() .child( h_flex() diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index af52d4dc162c93..ea0dc4a12c3da2 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -349,7 +349,11 @@ impl Dock { .and_then(|index| self.panel_entries.get(index)) } - pub(crate) fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { + pub fn active_panel_index(&self) -> Option { + self.active_panel_index + } + + pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { if open != self.is_open { self.is_open = open; if let Some(active_panel) = self.active_panel_entry() { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index adb1d2b03f490d..cfd55155799f3f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -22,6 +22,7 @@ use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag}; use futures::{channel::mpsc, select_biased, StreamExt}; +use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, @@ -927,6 +928,8 @@ fn initialize_pane( toolbar.add_item(syntax_tree_item, window, cx); let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx)); toolbar.add_item(migration_banner, window, cx); + let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); + toolbar.add_item(project_diff_toolbar, window, cx); }) }); } From 5e1dd91ee5c69c36cdce17acbd215251c33f5d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos?= Date: Fri, 21 Feb 2025 06:24:02 -0300 Subject: [PATCH 03/17] Fix UI font size changes not applying (#25307) Related to #24857. Release Notes: - N/A --- crates/assistant/src/inline_assistant.rs | 2 +- .../src/terminal_inline_assistant.rs | 2 +- crates/assistant2/src/inline_prompt_editor.rs | 2 +- crates/editor/src/commit_tooltip.rs | 2 +- crates/editor/src/editor_settings_controls.rs | 3 +-- crates/editor/src/hover_popover.rs | 2 +- crates/editor/src/signature_help.rs | 2 +- crates/git_ui/src/commit_modal.rs | 2 +- crates/recent_projects/src/ssh_connections.rs | 2 +- crates/repl/src/notebook/cell.rs | 2 +- crates/repl/src/outputs/table.rs | 2 +- .../src/appearance_settings_controls.rs | 3 +-- crates/theme/src/settings.rs | 20 +++++++++++++++---- crates/theme/src/theme.rs | 4 ++-- crates/ui/src/components/context_menu.rs | 2 +- crates/ui/src/styles/typography.rs | 4 ++-- crates/ui_macros/src/dynamic_spacing.rs | 2 +- crates/zed/src/zed/linux_prompts.rs | 2 +- 18 files changed, 35 insertions(+), 25 deletions(-) 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/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_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/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 index e61b8faa4a37bc..6cc67e11496dbf 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -141,7 +141,7 @@ impl CommitModal { let settings = ThemeSettings::get_global(cx); let line_height = relative(settings.buffer_line_height.value()) - .to_pixels(settings.buffer_font_size.into(), window.rem_size()); + .to_pixels(settings.buffer_font_size(cx).into(), window.rem_size()); v_flex() .justify_between() 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/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/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 03e5fa407db791..4bd69f506cd425 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -507,7 +507,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/styles/typography.rs b/crates/ui/src/styles/typography.rs index c575e734756ac6..d0f05b6cb2355c 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -140,8 +140,8 @@ impl TextSize { Self::Default => rems_from_px(14.), Self::Small => rems_from_px(12.), Self::XSmall => rems_from_px(10.), - Self::Ui => rems_from_px(theme_settings.ui_font_size.into()), - Self::Editor => rems_from_px(theme_settings.buffer_font_size.into()), + Self::Ui => rems_from_px(theme_settings.ui_font_size(cx).into()), + Self::Editor => rems_from_px(theme_settings.buffer_font_size(cx).into()), } } } diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs index d2d6d5d23ba6c2..a8890aca6b71f4 100644 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ b/crates/ui_macros/src/dynamic_spacing.rs @@ -157,7 +157,7 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream { /// Returns the spacing value in pixels. pub fn px(&self, cx: &App) -> Pixels { - let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size.into(); + let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size(cx).into(); px(ui_font_size_f32 * self.spacing_ratio(cx)) } } diff --git a/crates/zed/src/zed/linux_prompts.rs b/crates/zed/src/zed/linux_prompts.rs index 09d1eabf84a8a5..e6c2bf62b1c882 100644 --- a/crates/zed/src/zed/linux_prompts.rs +++ b/crates/zed/src/zed/linux_prompts.rs @@ -39,7 +39,7 @@ pub fn fallback_prompt_renderer( let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { font_family: Some(settings.ui_font.family.clone()), - font_size: Some(settings.ui_font_size.into()), + font_size: Some(settings.ui_font_size(cx).into()), color: Some(ui::Color::Muted.color(cx)), ..Default::default() }); From d45aaa174563cbeb0d082aa7557f1a40346b2bc9 Mon Sep 17 00:00:00 2001 From: smit <0xtimsb@gmail.com> Date: Fri, 21 Feb 2025 15:21:26 +0530 Subject: [PATCH 04/17] scrollbar: Implement minimum thumb size (#25288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR addresses 3 issues with the common scrollbar component used in the Terminal, Outline Panel, etc. 1. Extremely small or invisible scrollbar for long content. 2. Flickering issue when the thumb is already at the bottom-most position, and the user tries to overscroll. 3. Scrollbar appearing even when there is no excessive content to scroll. Before: image After: Screenshot 2025-02-21 at 3 26 32 AM Release Notes: - Fixed extremely small scrollbar thumb for long content in Terminal, Outline Panel, and more. --------- Co-authored-by: Danilo Co-authored-by: Richard Feldman --- .../terminal_view/src/terminal_scrollbar.rs | 5 +- crates/ui/src/components/scrollbar.rs | 351 +++++++++--------- 2 files changed, 184 insertions(+), 172 deletions(-) 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/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index a9793adea81e23..3775a0d3397c99 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -99,7 +99,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 +128,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 +145,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) } } @@ -228,174 +224,189 @@ impl Element for Scrollbar { fn paint( &mut self, _id: Option<&GlobalElementId>, - bounds: Bounds, + padded_bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let colors = cx.theme().colors(); - let thumb_background = colors - .surface_background - .blend(colors.scrollbar_thumb_background); - let is_vertical = self.kind == ScrollbarAxis::Vertical; - let extra_padding = px(5.0); - let padded_bounds = if is_vertical { - Bounds::from_corners( - bounds.origin + point(Pixels::ZERO, extra_padding), - bounds.bottom_right() - point(Pixels::ZERO, extra_padding * 3), - ) - } else { - Bounds::from_corners( - bounds.origin + point(extra_padding, Pixels::ZERO), - bounds.bottom_right() - point(extra_padding * 3, Pixels::ZERO), - ) - }; - - let mut thumb_bounds = if is_vertical { - let thumb_offset = self.thumb.start * padded_bounds.size.height; - let thumb_end = self.thumb.end * padded_bounds.size.height; - let thumb_upper_left = point( - padded_bounds.origin.x, - padded_bounds.origin.y + thumb_offset, - ); - let thumb_lower_right = point( - padded_bounds.origin.x + padded_bounds.size.width, - padded_bounds.origin.y + thumb_end, - ); - Bounds::from_corners(thumb_upper_left, thumb_lower_right) - } else { - let thumb_offset = self.thumb.start * padded_bounds.size.width; - let thumb_end = self.thumb.end * padded_bounds.size.width; - let thumb_upper_left = point( - padded_bounds.origin.x + thumb_offset, - padded_bounds.origin.y, - ); - let thumb_lower_right = point( - padded_bounds.origin.x + thumb_end, - padded_bounds.origin.y + padded_bounds.size.height, - ); - Bounds::from_corners(thumb_upper_left, thumb_lower_right) - }; - let corners = if is_vertical { - thumb_bounds.size.width /= 1.5; - Corners::all(thumb_bounds.size.width / 2.0) - } else { - thumb_bounds.size.height /= 1.5; - Corners::all(thumb_bounds.size.height / 2.0) - }; - window.paint_quad(quad( - thumb_bounds, - corners, - thumb_background, - Edges::default(), - Hsla::transparent_black(), - )); - - let scroll = self.state.scroll_handle.clone(); - let kind = self.kind; - let thumb_percentage_size = self.thumb.end - self.thumb.start; - - window.on_mouse_event({ - let scroll = scroll.clone(); - let state = self.state.clone(); + window.with_content_mask( + Some(ContentMask { + bounds: padded_bounds, + }), + |window| { + let colors = cx.theme().colors(); + let thumb_background = colors + .surface_background + .blend(colors.scrollbar_thumb_background); + let is_vertical = self.kind == ScrollbarAxis::Vertical; + let extra_padding = px(5.0); + let padded_bounds = if is_vertical { + Bounds::from_corners( + padded_bounds.origin + point(Pixels::ZERO, extra_padding), + padded_bounds.bottom_right() - point(Pixels::ZERO, extra_padding * 3), + ) + } else { + Bounds::from_corners( + padded_bounds.origin + point(extra_padding, Pixels::ZERO), + padded_bounds.bottom_right() - point(extra_padding * 3, Pixels::ZERO), + ) + }; + + let mut thumb_bounds = if is_vertical { + let thumb_offset = self.thumb.start * padded_bounds.size.height; + let thumb_end = self.thumb.end * padded_bounds.size.height; + let thumb_upper_left = point( + padded_bounds.origin.x, + padded_bounds.origin.y + thumb_offset, + ); + let thumb_lower_right = point( + padded_bounds.origin.x + padded_bounds.size.width, + padded_bounds.origin.y + thumb_end, + ); + Bounds::from_corners(thumb_upper_left, thumb_lower_right) + } else { + let thumb_offset = self.thumb.start * padded_bounds.size.width; + let thumb_end = self.thumb.end * padded_bounds.size.width; + let thumb_upper_left = point( + padded_bounds.origin.x + thumb_offset, + padded_bounds.origin.y, + ); + let thumb_lower_right = point( + padded_bounds.origin.x + thumb_end, + padded_bounds.origin.y + padded_bounds.size.height, + ); + Bounds::from_corners(thumb_upper_left, thumb_lower_right) + }; + let corners = if is_vertical { + thumb_bounds.size.width /= 1.5; + Corners::all(thumb_bounds.size.width / 2.0) + } else { + thumb_bounds.size.height /= 1.5; + Corners::all(thumb_bounds.size.height / 2.0) + }; + window.paint_quad(quad( + thumb_bounds, + corners, + thumb_background, + Edges::default(), + Hsla::transparent_black(), + )); + + let scroll = self.state.scroll_handle.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)); - } else if let Some(ContentSize { - size: item_size, .. - }) = scroll.content_size() - { - match kind { - 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)); - } - 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)); + window.on_mouse_event({ + let scroll = scroll.clone(); + let state = self.state.clone(); + move |event: &MouseDownEvent, phase, _, _| { + if !(phase.bubble() && padded_bounds.contains(&event.position)) { + return; + } + + if thumb_bounds.contains(&event.position) { + 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() + { + 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 => { + scroll.set_offset(point(click_offset, scroll.offset().y)); + } + ScrollbarAxis::Vertical => { + scroll.set_offset(point(scroll.offset().x, click_offset)); + } } } } - } - }); - window.on_mouse_event({ - let scroll = scroll.clone(); - move |event: &ScrollWheelEvent, phase, window, _| { - if phase.bubble() && bounds.contains(&event.position) { - let current_offset = scroll.offset(); - scroll.set_offset( - current_offset + event.delta.pixel_delta(window.line_height()), - ); + }); + window.on_mouse_event({ + let scroll = scroll.clone(); + move |event: &ScrollWheelEvent, phase, window, _| { + if phase.bubble() && padded_bounds.contains(&event.position) { + let current_offset = scroll.offset(); + scroll.set_offset( + current_offset + event.delta.pixel_delta(window.line_height()), + ); + } } - } - }); - let state = self.state.clone(); - let kind = 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 { - 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)); - } - 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)); + }); + let state = self.state.clone(); + 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() + { + 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 => { + scroll.set_offset(point(drag_offset, scroll.offset().y)); + } + ScrollbarAxis::Vertical => { + scroll.set_offset(point(scroll.offset().x, drag_offset)); + } + }; + if let Some(id) = state.parent_id { + cx.notify(id); } - }; - + } + } else { + state.drag.set(None); + } + }); + let state = self.state.clone(); + window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| { + if phase.bubble() { + state.drag.take(); if let Some(id) = state.parent_id { cx.notify(id); } } - } else { - state.drag.set(None); - } - }); - let state = self.state.clone(); - window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| { - if phase.bubble() { - state.drag.take(); - if let Some(id) = state.parent_id { - cx.notify(id); - } - } - }); - }) + }); + }, + ) } } From dff47a843695d03160680502e6d94634e376698e Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Fri, 21 Feb 2025 10:57:09 +0100 Subject: [PATCH 05/17] rust: Add support for doctest runnables (#24806) Screenshot: ![image](https://github.com/user-attachments/assets/0ac88029-76c1-4135-bef2-373636e3587d) I would be happy to add tests if you point me to the right place to do it please. Release Notes: - Added support for doc test in tasks for Rust --------- Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- crates/languages/src/rust.rs | 20 ++++++++++++ crates/languages/src/rust/runnables.scm | 42 ++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) 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/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 ( ( From c9235ff916e4717a75d9abfc4a91df92c022f980 Mon Sep 17 00:00:00 2001 From: smit <0xtimsb@gmail.com> Date: Fri, 21 Feb 2025 18:44:23 +0530 Subject: [PATCH 06/17] Revert unintended renaming (#25318) Just little bit clean up from #25288 Release Notes: - N/A --- crates/ui/src/components/scrollbar.rs | 324 +++++++++++++------------- 1 file changed, 158 insertions(+), 166 deletions(-) diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 3775a0d3397c99..c9864888302d42 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -224,189 +224,181 @@ impl Element for Scrollbar { fn paint( &mut self, _id: Option<&GlobalElementId>, - padded_bounds: Bounds, + bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { - window.with_content_mask( - Some(ContentMask { - bounds: padded_bounds, - }), - |window| { - let colors = cx.theme().colors(); - let thumb_background = colors - .surface_background - .blend(colors.scrollbar_thumb_background); - let is_vertical = self.kind == ScrollbarAxis::Vertical; - let extra_padding = px(5.0); - let padded_bounds = if is_vertical { - Bounds::from_corners( - padded_bounds.origin + point(Pixels::ZERO, extra_padding), - padded_bounds.bottom_right() - point(Pixels::ZERO, extra_padding * 3), - ) - } else { - Bounds::from_corners( - padded_bounds.origin + point(extra_padding, Pixels::ZERO), - padded_bounds.bottom_right() - point(extra_padding * 3, Pixels::ZERO), - ) - }; - - let mut thumb_bounds = if is_vertical { - let thumb_offset = self.thumb.start * padded_bounds.size.height; - let thumb_end = self.thumb.end * padded_bounds.size.height; - let thumb_upper_left = point( - padded_bounds.origin.x, - padded_bounds.origin.y + thumb_offset, - ); - let thumb_lower_right = point( - padded_bounds.origin.x + padded_bounds.size.width, - padded_bounds.origin.y + thumb_end, - ); - Bounds::from_corners(thumb_upper_left, thumb_lower_right) - } else { - let thumb_offset = self.thumb.start * padded_bounds.size.width; - let thumb_end = self.thumb.end * padded_bounds.size.width; - let thumb_upper_left = point( - padded_bounds.origin.x + thumb_offset, - padded_bounds.origin.y, - ); - let thumb_lower_right = point( - padded_bounds.origin.x + thumb_end, - padded_bounds.origin.y + padded_bounds.size.height, - ); - Bounds::from_corners(thumb_upper_left, thumb_lower_right) - }; - let corners = if is_vertical { - thumb_bounds.size.width /= 1.5; - Corners::all(thumb_bounds.size.width / 2.0) - } else { - thumb_bounds.size.height /= 1.5; - Corners::all(thumb_bounds.size.height / 2.0) - }; - window.paint_quad(quad( - thumb_bounds, - corners, - thumb_background, - Edges::default(), - Hsla::transparent_black(), - )); - - let scroll = self.state.scroll_handle.clone(); - let axis = self.kind; - - window.on_mouse_event({ - let scroll = scroll.clone(); - let state = self.state.clone(); - move |event: &MouseDownEvent, phase, _, _| { - if !(phase.bubble() && padded_bounds.contains(&event.position)) { - return; - } + window.with_content_mask(Some(ContentMask { bounds }), |window| { + let colors = cx.theme().colors(); + let thumb_background = colors + .surface_background + .blend(colors.scrollbar_thumb_background); + let is_vertical = self.kind == ScrollbarAxis::Vertical; + let extra_padding = px(5.0); + let padded_bounds = if is_vertical { + Bounds::from_corners( + bounds.origin + point(Pixels::ZERO, extra_padding), + bounds.bottom_right() - point(Pixels::ZERO, extra_padding * 3), + ) + } else { + Bounds::from_corners( + bounds.origin + point(extra_padding, Pixels::ZERO), + bounds.bottom_right() - point(extra_padding * 3, Pixels::ZERO), + ) + }; + + let mut thumb_bounds = if is_vertical { + let thumb_offset = self.thumb.start * padded_bounds.size.height; + let thumb_end = self.thumb.end * padded_bounds.size.height; + let thumb_upper_left = point( + padded_bounds.origin.x, + padded_bounds.origin.y + thumb_offset, + ); + let thumb_lower_right = point( + padded_bounds.origin.x + padded_bounds.size.width, + padded_bounds.origin.y + thumb_end, + ); + Bounds::from_corners(thumb_upper_left, thumb_lower_right) + } else { + let thumb_offset = self.thumb.start * padded_bounds.size.width; + let thumb_end = self.thumb.end * padded_bounds.size.width; + let thumb_upper_left = point( + padded_bounds.origin.x + thumb_offset, + padded_bounds.origin.y, + ); + let thumb_lower_right = point( + padded_bounds.origin.x + thumb_end, + padded_bounds.origin.y + padded_bounds.size.height, + ); + Bounds::from_corners(thumb_upper_left, thumb_lower_right) + }; + let corners = if is_vertical { + thumb_bounds.size.width /= 1.5; + Corners::all(thumb_bounds.size.width / 2.0) + } else { + thumb_bounds.size.height /= 1.5; + Corners::all(thumb_bounds.size.height / 2.0) + }; + window.paint_quad(quad( + thumb_bounds, + corners, + thumb_background, + Edges::default(), + Hsla::transparent_black(), + )); + + let scroll = self.state.scroll_handle.clone(); + let axis = self.kind; + + window.on_mouse_event({ + let scroll = scroll.clone(); + let state = self.state.clone(); + move |event: &MouseDownEvent, phase, _, _| { + if !(phase.bubble() && bounds.contains(&event.position)) { + return; + } - if thumb_bounds.contains(&event.position) { - 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() - { - 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 + if thumb_bounds.contains(&event.position) { + 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() + { + 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. }; - match axis { - ScrollbarAxis::Horizontal => { - scroll.set_offset(point(click_offset, scroll.offset().y)); - } - ScrollbarAxis::Vertical => { - scroll.set_offset(point(scroll.offset().x, click_offset)); - } + + -max_offset * percentage + }; + match axis { + ScrollbarAxis::Horizontal => { + scroll.set_offset(point(click_offset, scroll.offset().y)); + } + ScrollbarAxis::Vertical => { + scroll.set_offset(point(scroll.offset().x, click_offset)); } } } - }); - window.on_mouse_event({ - let scroll = scroll.clone(); - move |event: &ScrollWheelEvent, phase, window, _| { - if phase.bubble() && padded_bounds.contains(&event.position) { - let current_offset = scroll.offset(); - scroll.set_offset( - current_offset + event.delta.pixel_delta(window.line_height()), - ); - } + } + }); + window.on_mouse_event({ + let scroll = scroll.clone(); + move |event: &ScrollWheelEvent, phase, window, _| { + if phase.bubble() && bounds.contains(&event.position) { + let current_offset = scroll.offset(); + scroll.set_offset( + current_offset + event.delta.pixel_delta(window.line_height()), + ); } - }); - let state = self.state.clone(); - 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() - { - 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 + } + }); + let state = self.state.clone(); + 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() + { + 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. }; - match axis { - ScrollbarAxis::Horizontal => { - scroll.set_offset(point(drag_offset, scroll.offset().y)); - } - ScrollbarAxis::Vertical => { - scroll.set_offset(point(scroll.offset().x, drag_offset)); - } - }; - if let Some(id) = state.parent_id { - cx.notify(id); + + -max_offset * percentage + }; + match axis { + ScrollbarAxis::Horizontal => { + scroll.set_offset(point(drag_offset, scroll.offset().y)); } - } - } else { - state.drag.set(None); - } - }); - let state = self.state.clone(); - window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| { - if phase.bubble() { - state.drag.take(); + ScrollbarAxis::Vertical => { + scroll.set_offset(point(scroll.offset().x, drag_offset)); + } + }; if let Some(id) = state.parent_id { cx.notify(id); } } - }); - }, - ) + } else { + state.drag.set(None); + } + }); + let state = self.state.clone(); + window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| { + if phase.bubble() { + state.drag.take(); + if let Some(id) = state.parent_id { + cx.notify(id); + } + } + }); + }) } } From 5397ca23a17ae42ed0e5c75ae3e4d2733b94fa36 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 21 Feb 2025 09:20:53 -0500 Subject: [PATCH 07/17] ui: More component previews, UI component cleanup (#25302) - Don't require ui component docs (this isn't really working) - Add more component previews - Update component preview style & navigation Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 4 +- crates/component/src/component.rs | 15 +- .../src/component_preview.rs | 186 ++++++----- crates/storybook/src/story_selector.rs | 4 - crates/ui/docs/building-ui.md | 49 --- crates/ui/docs/hello-world.md | 160 --------- crates/ui/src/components/avatar.rs | 307 +++++++++++++++++- crates/ui/src/components/avatar/avatar.rs | 156 --------- .../avatar/avatar_audio_status_indicator.rs | 72 ---- .../avatar/avatar_availability_indicator.rs | 49 --- crates/ui/src/components/button/button.rs | 26 +- .../ui/src/components/button/button_icon.rs | 1 - .../ui/src/components/button/button_like.rs | 1 - .../ui/src/components/button/icon_button.rs | 163 +++++++++- .../ui/src/components/button/toggle_button.rs | 131 +++++++- crates/ui/src/components/context_menu.rs | 1 - crates/ui/src/components/disclosure.rs | 1 - crates/ui/src/components/divider.rs | 1 - crates/ui/src/components/dropdown_menu.rs | 1 - crates/ui/src/components/facepile.rs | 113 ++++--- crates/ui/src/components/icon.rs | 2 - crates/ui/src/components/image.rs | 1 - crates/ui/src/components/indent_guides.rs | 1 - crates/ui/src/components/indicator.rs | 1 - crates/ui/src/components/keybinding.rs | 1 - .../src/components/label/highlighted_label.rs | 2 - crates/ui/src/components/list/list.rs | 2 - crates/ui/src/components/list/list_header.rs | 2 - crates/ui/src/components/list/list_item.rs | 2 - .../ui/src/components/list/list_separator.rs | 2 - .../ui/src/components/list/list_sub_header.rs | 2 - crates/ui/src/components/modal.rs | 2 - crates/ui/src/components/numeric_stepper.rs | 2 - crates/ui/src/components/popover.rs | 2 - crates/ui/src/components/popover_menu.rs | 2 - crates/ui/src/components/radio.rs | 2 - crates/ui/src/components/right_click_menu.rs | 2 - crates/ui/src/components/scrollbar.rs | 1 - .../ui/src/components/settings_container.rs | 2 - crates/ui/src/components/settings_group.rs | 2 - crates/ui/src/components/stack.rs | 2 - crates/ui/src/components/stories.rs | 7 - crates/ui/src/components/stories/avatar.rs | 64 ---- crates/ui/src/components/stories/button.rs | 38 --- crates/ui/src/components/tab.rs | 1 - crates/ui/src/components/tab_bar.rs | 1 - crates/ui/src/components/tooltip.rs | 2 - crates/ui/src/styles/typography.rs | 65 ++-- crates/ui/src/ui.rs | 2 - crates/ui/src/utils/format_distance.rs | 1 - crates/ui/src/utils/search_input.rs | 2 - crates/workspace/src/theme_preview.rs | 8 +- 52 files changed, 813 insertions(+), 856 deletions(-) delete mode 100644 crates/ui/docs/building-ui.md delete mode 100644 crates/ui/docs/hello-world.md delete mode 100644 crates/ui/src/components/avatar/avatar.rs delete mode 100644 crates/ui/src/components/avatar/avatar_audio_status_indicator.rs delete mode 100644 crates/ui/src/components/avatar/avatar_availability_indicator.rs 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/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/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/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/context_menu.rs b/crates/ui/src/components/context_menu.rs index 4bd69f506cd425..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, 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 c6e31ee7deb569..6c539704237568 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -1,4 +1,3 @@ -#![allow(missing_docs)] use gpui::{Hsla, IntoElement}; use crate::prelude::*; 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/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 c9864888302d42..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}; 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::