From 801125974ae1083223c7d97c2bb27e7e8dabaa3c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Dec 2023 15:32:32 +0100 Subject: [PATCH 01/15] Optimize inserting lots of primitives with the same StackingOrder --- crates/gpui2/src/scene.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index ca0a50546e..fd63b49a1a 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -17,6 +17,7 @@ pub type LayerId = u32; pub type DrawOrder = u32; pub(crate) struct SceneBuilder { + last_order: Option<(StackingOrder, LayerId)>, layers_by_order: BTreeMap, splitter: BspSplitter<(PrimitiveKind, usize)>, shadows: Vec, @@ -31,6 +32,7 @@ pub(crate) struct SceneBuilder { impl Default for SceneBuilder { fn default() -> Self { SceneBuilder { + last_order: None, layers_by_order: BTreeMap::new(), splitter: BspSplitter::new(), shadows: Vec::new(), @@ -156,14 +158,7 @@ impl SceneBuilder { return; } - let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { - *layer_id - } else { - let next_id = self.layers_by_order.len() as LayerId; - self.layers_by_order.insert(order.clone(), next_id); - next_id - }; - + let layer_id = self.layer_id_for_order(order); match primitive { Primitive::Shadow(mut shadow) => { shadow.order = layer_id; @@ -196,6 +191,24 @@ impl SceneBuilder { } } } + + fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 { + if let Some((last_order, last_layer_id)) = self.last_order.as_ref() { + if last_order == order { + return *last_layer_id; + } + }; + + let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) { + *layer_id + } else { + let next_id = self.layers_by_order.len() as LayerId; + self.layers_by_order.insert(order.clone(), next_id); + next_id + }; + self.last_order = Some((order.clone(), layer_id)); + layer_id + } } pub struct Scene { From 9ff73d3a0a9042a99ee856b22dd6ed8d2f2bf356 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 13 Dec 2023 13:35:49 -0700 Subject: [PATCH 02/15] Port project_symbols --- Cargo.lock | 25 ++ Cargo.toml | 1 + crates/picker2/Cargo.toml | 1 + crates/picker2/src/picker2.rs | 16 +- crates/project_symbols2/Cargo.toml | 37 ++ .../project_symbols2/src/project_symbols.rs | 411 ++++++++++++++++++ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 8 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 crates/project_symbols2/Cargo.toml create mode 100644 crates/project_symbols2/src/project_symbols.rs diff --git a/Cargo.lock b/Cargo.lock index 033ff8b69c..b29c28c6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6510,6 +6510,7 @@ dependencies = [ "theme2", "ui2", "util", + "workspace2", ] [[package]] @@ -7019,6 +7020,29 @@ dependencies = [ "workspace", ] +[[package]] +name = "project_symbols2" +version = "0.1.0" +dependencies = [ + "anyhow", + "editor2", + "futures 0.3.28", + "fuzzy2", + "gpui2", + "language2", + "lsp2", + "ordered-float 2.10.0", + "picker2", + "postage", + "project2", + "settings2", + "smol", + "text2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "prometheus" version = "0.13.3" @@ -12081,6 +12105,7 @@ dependencies = [ "postage", "project2", "project_panel2", + "project_symbols2", "quick_action_bar2", "rand 0.8.5", "recent_projects2", diff --git a/Cargo.toml b/Cargo.toml index 2190066df5..95cf2ae78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "crates/project_panel", "crates/project_panel2", "crates/project_symbols", + "crates/project_symbols2", "crates/quick_action_bar2", "crates/recent_projects", "crates/recent_projects2", diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 3c4d21ad50..e94702ff9c 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2"} parking_lot.workspace = true diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index db5eebff53..6543eb7213 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,11 +1,12 @@ use editor::Editor; use gpui::{ div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter, - FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, - View, ViewContext, WindowContext, + FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, + UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; +use workspace::ModalView; pub struct Picker { pub delegate: D, @@ -13,6 +14,7 @@ pub struct Picker { editor: View, pending_update_matches: Option>, confirm_on_update: Option, + width: Option, } pub trait PickerDelegate: Sized + 'static { @@ -55,11 +57,17 @@ impl Picker { scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, confirm_on_update: None, + width: None, }; this.update_matches("".to_string(), cx); this } + pub fn width(mut self, width: impl Into) -> Self { + self.width = Some(width.into()); + self + } + pub fn focus(&self, cx: &mut WindowContext) { self.editor.update(cx, |editor, cx| editor.focus(cx)); } @@ -197,6 +205,7 @@ impl Picker { } impl EventEmitter for Picker {} +impl ModalView for Picker {} impl Render for Picker { type Element = Div; @@ -221,6 +230,9 @@ impl Render for Picker { div() .key_context("picker") .size_full() + .when_some(self.width, |el, width| { + el.w(width) + }) .overflow_hidden() .elevation_3(cx) .on_action(cx.listener(Self::select_next)) diff --git a/crates/project_symbols2/Cargo.toml b/crates/project_symbols2/Cargo.toml new file mode 100644 index 0000000000..e11dd373a8 --- /dev/null +++ b/crates/project_symbols2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "project_symbols2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project_symbols.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = {package = "fuzzy2", path = "../fuzzy2" } +gpui = {package = "gpui2", path = "../gpui2" } +picker = {package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +text = {package = "text2", path = "../text2" } +settings = {package = "settings2", path = "../settings2" } +workspace = {package = "workspace2", path = "../workspace2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } + +anyhow.workspace = true +ordered-float.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +futures.workspace = true +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/project_symbols2/src/project_symbols.rs b/crates/project_symbols2/src/project_symbols.rs new file mode 100644 index 0000000000..da67fc888f --- /dev/null +++ b/crates/project_symbols2/src/project_symbols.rs @@ -0,0 +1,411 @@ +use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, + View, ViewContext, WeakView, +}; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use project::{Project, Symbol}; +use std::{borrow::Cow, cmp::Reverse, sync::Arc}; +use theme::ActiveTheme; +use util::ResultExt; +use workspace::{ + ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable}, + Workspace, +}; + +actions!(project_symbols, [Toggle]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace.register_action(|workspace, _: &Toggle, cx| { + let project = workspace.project().clone(); + let handle = cx.view().downgrade(); + workspace.toggle_modal(cx, move |cx| { + let delegate = ProjectSymbolsDelegate::new(handle, project); + Picker::new(delegate, cx).width(rems(34.)) + }) + }); + }, + ) + .detach(); +} + +pub type ProjectSymbols = View>; + +pub struct ProjectSymbolsDelegate { + workspace: WeakView, + project: Model, + selected_match_index: usize, + symbols: Vec, + visible_match_candidates: Vec, + external_match_candidates: Vec, + show_worktree_root_name: bool, + matches: Vec, +} + +impl ProjectSymbolsDelegate { + fn new(workspace: WeakView, project: Model) -> Self { + Self { + workspace, + project, + selected_match_index: 0, + symbols: Default::default(), + visible_match_candidates: Default::default(), + external_match_candidates: Default::default(), + matches: Default::default(), + show_worktree_root_name: false, + } + } + + fn filter(&mut self, query: &str, cx: &mut ViewContext>) { + const MAX_MATCHES: usize = 100; + let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( + &self.visible_match_candidates, + query, + false, + MAX_MATCHES, + &Default::default(), + cx.background_executor().clone(), + )); + let mut external_matches = cx.background_executor().block(fuzzy::match_strings( + &self.external_match_candidates, + query, + false, + MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), + &Default::default(), + cx.background_executor().clone(), + )); + let sort_key_for_match = |mat: &StringMatch| { + let symbol = &self.symbols[mat.candidate_id]; + ( + Reverse(OrderedFloat(mat.score)), + &symbol.label.text[symbol.label.filter_range.clone()], + ) + }; + + visible_matches.sort_unstable_by_key(sort_key_for_match); + external_matches.sort_unstable_by_key(sort_key_for_match); + let mut matches = visible_matches; + matches.append(&mut external_matches); + + for mat in &mut matches { + let symbol = &self.symbols[mat.candidate_id]; + let filter_start = symbol.label.filter_range.start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches; + self.set_selected_index(0, cx); + } +} + +impl PickerDelegate for ProjectSymbolsDelegate { + type ListItem = ListItem; + fn placeholder_text(&self) -> Arc { + "Search project symbols...".into() + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + if let Some(symbol) = self + .matches + .get(self.selected_match_index) + .map(|mat| self.symbols[mat.candidate_id].clone()) + { + let buffer = self.project.update(cx, |project, cx| { + project.open_buffer_for_symbol(&symbol, cx) + }); + let symbol = symbol.clone(); + let workspace = self.workspace.clone(); + cx.spawn(|_, mut cx| async move { + let buffer = buffer.await?; + workspace.update(&mut cx, |workspace, cx| { + let position = buffer + .read(cx) + .clip_point_utf16(symbol.range.start, Bias::Left); + + let editor = if secondary { + workspace.split_project_item::(buffer, cx) + } else { + workspace.open_project_item::(buffer, cx) + }; + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([position..position]) + }); + }); + })?; + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + cx.emit(DismissEvent); + } + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + self.selected_match_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + self.filter(&query, cx); + self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; + let symbols = self + .project + .update(cx, |project, cx| project.symbols(&query, cx)); + cx.spawn(|this, mut cx| async move { + let symbols = symbols.await.log_err(); + if let Some(symbols) = symbols { + this.update(&mut cx, |this, cx| { + let delegate = &mut this.delegate; + let project = delegate.project.read(cx); + let (visible_match_candidates, external_match_candidates) = symbols + .iter() + .enumerate() + .map(|(id, symbol)| { + StringMatchCandidate::new( + id, + symbol.label.text[symbol.label.filter_range.clone()].to_string(), + ) + }) + .partition(|candidate| { + project + .entry_for_path(&symbols[candidate.id].path, cx) + .map_or(false, |e| !e.is_ignored) + }); + + delegate.visible_match_candidates = visible_match_candidates; + delegate.external_match_candidates = external_match_candidates; + delegate.symbols = symbols; + delegate.filter(&query, cx); + }) + .log_err(); + } + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let string_match = &self.matches[ix]; + let symbol = &self.symbols[string_match.candidate_id]; + let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax()); + + let mut path = symbol.path.path.to_string_lossy(); + if self.show_worktree_root_name { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) { + path = Cow::Owned(format!( + "{}{}{}", + worktree.read(cx).root_name(), + std::path::MAIN_SEPARATOR, + path.as_ref() + )); + } + } + let label = symbol.label.text.clone(); + let path = path.to_string().clone(); + + let highlights = gpui::combine_highlights( + string_match + .positions + .iter() + .map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())), + syntax_runs.map(|(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) + }), + ); + + Some( + ListItem::new(ix).inset(true).selected(selected).child( + // todo!() combine_syntax_and_fuzzy_match_highlights() + v_stack() + .child( + LabelLike::new().child( + StyledText::new(label) + .with_highlights(&cx.text_style().clone(), highlights), + ), + ) + .child(Label::new(path).color(Color::Muted)), + ), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use gpui::{serde_json::json, TestAppContext, VisualContext}; + use language::{FakeLspAdapter, Language, LanguageConfig}; + use project::FakeFs; + use settings::SettingsStore; + use std::{path::Path, sync::Arc}; + + #[gpui::test] + async fn test_project_symbols(cx: &mut TestAppContext) { + init_test(cx); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::::default()) + .await; + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/test.rs", cx) + }) + .await + .unwrap(); + + // Set up fake language server to return fuzzy matches against + // a fixed set of symbol names. + let fake_symbols = [ + symbol("one", "/external"), + symbol("ton", "/dir/test.rs"), + symbol("uno", "/dir/test.rs"), + ]; + let fake_server = fake_servers.next().await.unwrap(); + fake_server.handle_request::( + move |params: lsp::WorkspaceSymbolParams, cx| { + let executor = cx.background_executor().clone(); + let fake_symbols = fake_symbols.clone(); + async move { + let candidates = fake_symbols + .iter() + .enumerate() + .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone())) + .collect::>(); + let matches = if params.query.is_empty() { + Vec::new() + } else { + fuzzy::match_strings( + &candidates, + ¶ms.query, + true, + 100, + &Default::default(), + executor.clone(), + ) + .await + }; + + Ok(Some(lsp::WorkspaceSymbolResponse::Flat( + matches + .into_iter() + .map(|mat| fake_symbols[mat.candidate_id].clone()) + .collect(), + ))) + } + }, + ); + + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // Create the project symbols view. + let symbols = cx.build_view(|cx| { + Picker::new( + ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), + cx, + ) + }); + + // Spawn multiples updates before the first update completes, + // such that in the end, there are no matches. Testing for regression: + // https://github.com/zed-industries/zed/issues/861 + symbols.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("on".to_string(), cx); + p.update_matches("onex".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + assert_eq!(symbols.delegate.matches.len(), 0); + }); + + // Spawn more updates such that in the end, there are matches. + symbols.update(cx, |p, cx| { + p.update_matches("one".to_string(), cx); + p.update_matches("on".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + let delegate = &symbols.delegate; + assert_eq!(delegate.matches.len(), 2); + assert_eq!(delegate.matches[0].string, "ton"); + assert_eq!(delegate.matches[1].string, "one"); + }); + + // Spawn more updates such that in the end, there are again no matches. + symbols.update(cx, |p, cx| { + p.update_matches("o".to_string(), cx); + p.update_matches("".to_string(), cx); + }); + + cx.run_until_parked(); + symbols.update(cx, |symbols, _| { + assert_eq!(symbols.delegate.matches.len(), 0); + }); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + }); + } + + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { + #[allow(deprecated)] + lsp::SymbolInformation { + name: name.to_string(), + kind: lsp::SymbolKind::FUNCTION, + tags: None, + deprecated: None, + container_name: None, + location: lsp::Location::new( + lsp::Url::from_file_path(path.as_ref()).unwrap(), + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), + ), + } + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 859afee4f7..6646eb5ffc 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } -# project_symbols = { path = "../project_symbols" } +project_symbols = { package = "project_symbols2", path = "../project_symbols2" } quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } recent_projects = { package = "recent_projects2", path = "../recent_projects2" } rope = { package = "rope2", path = "../rope2"} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index bbb8382cb2..ca8cd7a2a2 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -205,7 +205,7 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); outline::init(cx); - // project_symbols::init(cx); + project_symbols::init(cx); project_panel::init(Assets, cx); channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); From 06b9055e27d42fcbbd6181b1bba40ab5481c6cd6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 Dec 2023 22:02:30 +0100 Subject: [PATCH 03/15] Clear last_order when building Scene --- crates/gpui2/src/scene.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index fd63b49a1a..68c068dfe9 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -54,6 +54,7 @@ impl SceneBuilder { layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32; } self.layers_by_order.clear(); + self.last_order = None; // Add all primitives to the BSP splitter to determine draw order self.splitter.reset(); From ee509e043d359e354b975c45c4f2873c133a40d6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 16:08:31 -0500 Subject: [PATCH 04/15] Rework `ListItem` and `ListHeader` to use slot-based APIs (#3635) This PR reworks the `ListItem` and `ListHeader` components to use slot-based APIs, making them less opinionated about their contents. Splitting this out of the collab UI styling PR so we can land it to avoid conflicts. Co-authored-by: Nate Release Notes: - N/A --- crates/collab_ui2/src/collab_panel.rs | 28 +-- crates/picker2/src/picker2.rs | 1 - crates/ui2/src/components/context_menu.rs | 5 +- crates/ui2/src/components/list/list_header.rs | 67 +++++--- crates/ui2/src/components/list/list_item.rs | 161 ++++++++++++------ .../ui2/src/components/list/list_separator.rs | 6 +- .../src/components/list/list_sub_header.rs | 8 +- .../ui2/src/components/stories/list_header.rs | 12 +- .../ui2/src/components/stories/list_item.rs | 72 +++++++- crates/ui2/src/styled_ext.rs | 16 +- 10 files changed, 267 insertions(+), 109 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index ac7457abe0..a34d574957 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1156,7 +1156,7 @@ impl CollabPanel { let tooltip = format!("Follow {}", user.github_login); ListItem::new(SharedString::from(user.github_login.clone())) - .left_child(Avatar::new(user.avatar_uri.clone())) + .start_slot(Avatar::new(user.avatar_uri.clone())) .child( h_stack() .w_full() @@ -1212,7 +1212,7 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .left_child(render_tree_branch(is_last, cx)) + .start_slot(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Folder)) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -1305,7 +1305,7 @@ impl CollabPanel { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) - .left_child(render_tree_branch(is_last, cx)) + .start_slot(render_tree_branch(is_last, cx)) .child(IconButton::new(0, Icon::Screen)) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { @@ -1372,7 +1372,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) - .left_child(render_tree_branch(false, cx)) + .start_slot(render_tree_branch(false, cx)) .child(IconButton::new(0, Icon::File)) .child(Label::new("notes")) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) @@ -1387,7 +1387,7 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) - .left_child(render_tree_branch(true, cx)) + .start_slot(render_tree_branch(true, cx)) .child(IconButton::new(0, Icon::MessageBubbles)) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) @@ -2318,7 +2318,7 @@ impl CollabPanel { } else { el.child( ListHeader::new(text) - .when_some(button, |el, button| el.meta(button)) + .when_some(button, |el, button| el.end_slot(button)) .selected(is_selected), ) } @@ -2381,7 +2381,7 @@ impl CollabPanel { ) }), ) - .left_child( + .start_slot( // todo!() handle contacts with no avatar Avatar::new(contact.user.avatar_uri.clone()) .availability_indicator(if online { Some(!busy) } else { None }), @@ -2460,7 +2460,7 @@ impl CollabPanel { .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) - .left_avatar(user.avatar_uri.clone()) + .start_slot::(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar))) } fn render_contact_placeholder( @@ -2568,7 +2568,11 @@ impl CollabPanel { ListItem::new(channel_id as usize) .indent_level(depth) .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle - .left_icon(if is_public { Icon::Public } else { Icon::Hash }) + .start_slot( + IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) + .size(IconSize::Small) + .color(Color::Muted), + ) .selected(is_selected || is_active) .child( h_stack() @@ -2962,7 +2966,11 @@ impl CollabPanel { let item = ListItem::new("channel-editor") .inset(false) .indent_level(depth) - .left_icon(Icon::Hash); + .start_slot( + IconElement::new(Icon::Hash) + .size(IconSize::Small) + .color(Color::Muted), + ); if let Some(pending_name) = self .channel_editing_state diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index db5eebff53..8d80f4b36c 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -271,7 +271,6 @@ impl Render for Picker { }, ) .track_scroll(self.scroll_handle.clone()) - .p_1() ) .max_h_72() .overflow_hidden(), diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 3e54298514..250272b198 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -255,6 +255,9 @@ impl Render for ContextMenu { }; ListItem::new(label.clone()) + .inset(true) + .selected(Some(ix) == self.selected_index) + .on_click(move |_, cx| handler(cx)) .child( h_stack() .w_full() @@ -265,8 +268,6 @@ impl Render for ContextMenu { .map(|binding| div().ml_1().child(binding)) })), ) - .selected(Some(ix) == self.selected_index) - .on_click(move |_, cx| handler(cx)) .into_any_element() } }, diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 933a1a95d7..6c497752ae 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -1,12 +1,18 @@ -use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label}; +use crate::{h_stack, prelude::*, Disclosure, Label}; use gpui::{AnyElement, ClickEvent, Div}; -use smallvec::SmallVec; #[derive(IntoElement)] pub struct ListHeader { + /// The label of the header. label: SharedString, - left_icon: Option, - meta: SmallVec<[AnyElement; 2]>, + /// A slot for content that appears before the label, like an icon or avatar. + start_slot: Option, + /// A slot for content that appears after the label, usually on the other side of the header. + /// This might be a button, a disclosure arrow, a face pile, etc. + end_slot: Option, + /// A slot for content that appears on hover after the label + /// It will obscure the `end_slot` when visible. + end_hover_slot: Option, toggle: Option, on_toggle: Option>, inset: bool, @@ -17,8 +23,9 @@ impl ListHeader { pub fn new(label: impl Into) -> Self { Self { label: label.into(), - left_icon: None, - meta: SmallVec::new(), + start_slot: None, + end_slot: None, + end_hover_slot: None, inset: false, toggle: None, on_toggle: None, @@ -39,13 +46,23 @@ impl ListHeader { self } - pub fn left_icon(mut self, left_icon: impl Into>) -> Self { - self.left_icon = left_icon.into(); + pub fn start_slot(mut self, start_slot: impl Into>) -> Self { + self.start_slot = start_slot.into().map(IntoElement::into_any_element); self } - pub fn meta(mut self, meta: impl IntoElement) -> Self { - self.meta.push(meta.into_any_element()); + pub fn end_slot(mut self, end_slot: impl Into>) -> Self { + self.end_slot = end_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { + self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn inset(mut self, inset: bool) -> Self { + self.inset = inset; self } } @@ -61,9 +78,9 @@ impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - h_stack().w_full().relative().child( + h_stack().w_full().relative().group("list_header").child( div() - .h_5() + .h_7() .when(self.inset, |this| this.px_2()) .when(self.selected, |this| { this.bg(cx.theme().colors().ghost_element_selected) @@ -77,24 +94,30 @@ impl RenderOnce for ListHeader { .child( h_stack() .gap_1() + .children( + self.toggle + .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), + ) .child( div() .flex() .gap_1() .items_center() - .children(self.left_icon.map(|i| { - IconElement::new(i) - .color(Color::Muted) - .size(IconSize::Small) - })) + .children(self.start_slot) .child(Label::new(self.label.clone()).color(Color::Muted)), - ) - .children( - self.toggle - .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ), ) - .child(h_stack().gap_2().items_center().children(self.meta)), + .child(h_stack().children(self.end_slot)) + .when_some(self.end_hover_slot, |this, end_hover_slot| { + this.child( + div() + .invisible() + .group_hover("list_header", |this| this.visible()) + .absolute() + .right_0() + .child(end_hover_slot), + ) + }), ) } } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 28a8b8cecb..df6e542816 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,7 +1,6 @@ -use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize}; +use crate::{prelude::*, Disclosure}; use gpui::{ - px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, - Stateful, + px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful, }; use smallvec::SmallVec; @@ -9,11 +8,16 @@ use smallvec::SmallVec; pub struct ListItem { id: ElementId, selected: bool, - // TODO: Reintroduce this - // disclosure_control_style: DisclosureControlVisibility, indent_level: usize, indent_step_size: Pixels, - left_slot: Option, + /// A slot for content that appears before the children, like an icon or avatar. + start_slot: Option, + /// A slot for content that appears after the children, usually on the other side of the header. + /// This might be a button, a disclosure arrow, a face pile, etc. + end_slot: Option, + /// A slot for content that appears on hover after the children + /// It will obscure the `end_slot` when visible. + end_hover_slot: Option, toggle: Option, inset: bool, on_click: Option>, @@ -30,7 +34,9 @@ impl ListItem { selected: false, indent_level: 0, indent_step_size: px(12.), - left_slot: None, + start_slot: None, + end_slot: None, + end_hover_slot: None, toggle: None, inset: false, on_click: None, @@ -87,23 +93,18 @@ impl ListItem { self } - pub fn left_child(mut self, left_content: impl IntoElement) -> Self { - self.left_slot = Some(left_content.into_any_element()); + pub fn start_slot(mut self, start_slot: impl Into>) -> Self { + self.start_slot = start_slot.into().map(IntoElement::into_any_element); self } - pub fn left_icon(mut self, left_icon: Icon) -> Self { - self.left_slot = Some( - IconElement::new(left_icon) - .size(IconSize::Small) - .color(Color::Muted) - .into_any_element(), - ); + pub fn end_slot(mut self, end_slot: impl Into>) -> Self { + self.end_slot = end_slot.into().map(IntoElement::into_any_element); self } - pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { - self.left_slot = Some(Avatar::new(left_avatar).into_any_element()); + pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { + self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); self } } @@ -125,49 +126,105 @@ impl RenderOnce for ListItem { type Rendered = Stateful
; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .id(self.id) + h_stack() + .id("item_container") + .w_full() .relative() - // TODO: Add focus state - // .when(self.state == InteractionState::Focused, |this| { - // this.border() - // .border_color(cx.theme().colors().border_focused) - // }) - .when(self.inset, |this| this.rounded_md()) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) + // When an item is inset draw the indent spacing outside of the item + .when(self.inset, |this| { + this.ml(self.indent_level as f32 * self.indent_step_size) + .px_1() }) - .when_some(self.on_click, |this, on_click| { - this.cursor_pointer().on_click(move |event, cx| { - // HACK: GPUI currently fires `on_click` with any mouse button, - // but we only care about the left button. - if event.down.button == MouseButton::Left { - (on_click)(event, cx) - } - }) + .when(!self.inset, |this| { + this + // TODO: Add focus state + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(cx.theme().colors().border_focused) + // }) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) }) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, cx| { - (on_mouse_down)(event, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) .child( - div() - .when(self.inset, |this| this.px_2()) - .ml(self.indent_level as f32 * self.indent_step_size) - .flex() - .gap_1() - .items_center() + h_stack() + .id(self.id) + .w_full() .relative() + .gap_1() + .px_2() + .group("list_item") + .when(self.inset, |this| { + this + // TODO: Add focus state + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(cx.theme().colors().border_focused) + // }) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .when(self.selected, |this| { + this.bg(cx.theme().colors().ghost_element_selected) + }) + }) + .when_some(self.on_click, |this, on_click| { + this.cursor_pointer().on_click(move |event, cx| { + // HACK: GPUI currently fires `on_click` with any mouse button, + // but we only care about the left button. + if event.down.button == MouseButton::Left { + (on_click)(event, cx) + } + }) + }) + .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { + this.on_mouse_down(MouseButton::Right, move |event, cx| { + (on_mouse_down)(event, cx) + }) + }) + .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) + .map(|this| { + if self.inset { + this.rounded_md() + } else { + // When an item is not inset draw the indent spacing inside of the item + this.ml(self.indent_level as f32 * self.indent_step_size) + } + }) .children( self.toggle .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ) - .children(self.left_slot) - .children(self.children), + .child( + h_stack() + .flex_1() + .gap_1() + .children(self.start_slot) + .children(self.children), + ) + .when_some(self.end_slot, |this, end_slot| { + this.justify_between().child( + h_stack() + .when(self.end_hover_slot.is_some(), |this| { + this.visible() + .group_hover("list_item", |this| this.invisible()) + }) + .child(end_slot), + ) + }) + .when_some(self.end_hover_slot, |this, end_hover_slot| { + this.child( + h_stack() + .h_full() + .absolute() + .right_2() + .top_0() + .invisible() + .group_hover("list_item", |this| this.visible()) + .child(end_hover_slot), + ) + }), ) } } diff --git a/crates/ui2/src/components/list/list_separator.rs b/crates/ui2/src/components/list/list_separator.rs index 0398a110e9..346b13ddaa 100644 --- a/crates/ui2/src/components/list/list_separator.rs +++ b/crates/ui2/src/components/list/list_separator.rs @@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div().h_px().w_full().bg(cx.theme().colors().border_variant) + div() + .h_px() + .w_full() + .my_1() + .bg(cx.theme().colors().border_variant) } } diff --git a/crates/ui2/src/components/list/list_sub_header.rs b/crates/ui2/src/components/list/list_sub_header.rs index 17f07b7b0b..07a99dabe5 100644 --- a/crates/ui2/src/components/list/list_sub_header.rs +++ b/crates/ui2/src/components/list/list_sub_header.rs @@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label}; #[derive(IntoElement)] pub struct ListSubHeader { label: SharedString, - left_icon: Option, + start_slot: Option, inset: bool, } @@ -14,13 +14,13 @@ impl ListSubHeader { pub fn new(label: impl Into) -> Self { Self { label: label.into(), - left_icon: None, + start_slot: None, inset: false, } } pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; + self.start_slot = left_icon; self } } @@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader { .flex() .gap_1() .items_center() - .children(self.left_icon.map(|i| { + .children(self.start_slot.map(|i| { IconElement::new(i) .color(Color::Muted) .size(IconSize::Small) diff --git a/crates/ui2/src/components/stories/list_header.rs b/crates/ui2/src/components/stories/list_header.rs index 056eaa2762..3c80afdde3 100644 --- a/crates/ui2/src/components/stories/list_header.rs +++ b/crates/ui2/src/components/stories/list_header.rs @@ -15,19 +15,19 @@ impl Render for ListHeaderStory { .child(Story::label("Default")) .child(ListHeader::new("Section 1")) .child(Story::label("With left icon")) - .child(ListHeader::new("Section 2").left_icon(Icon::Bell)) + .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell))) .child(Story::label("With left icon and meta")) .child( ListHeader::new("Section 3") - .left_icon(Icon::BellOff) - .meta(IconButton::new("action_1", Icon::Bolt)), + .start_slot(IconElement::new(Icon::BellOff)) + .end_slot(IconButton::new("action_1", Icon::Bolt)), ) .child(Story::label("With multiple meta")) .child( ListHeader::new("Section 4") - .meta(IconButton::new("action_1", Icon::Bolt)) - .meta(IconButton::new("action_2", Icon::ExclamationTriangle)) - .meta(IconButton::new("action_3", Icon::Plus)), + .end_slot(IconButton::new("action_1", Icon::Bolt)) + .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle)) + .end_slot(IconButton::new("action_3", Icon::Plus)), ) } } diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index 91e95348fd..fbcea44b57 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -1,7 +1,7 @@ use gpui::{Div, Render}; use story::Story; -use crate::prelude::*; +use crate::{prelude::*, Avatar}; use crate::{Icon, ListItem}; pub struct ListItemStory; @@ -9,24 +9,80 @@ pub struct ListItemStory; impl Render for ListItemStory { type Element = Div; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { Story::container() + .bg(cx.theme().colors().background) .child(Story::title_for::()) .child(Story::label("Default")) .child(ListItem::new("hello_world").child("Hello, world!")) - .child(Story::label("With left icon")) + .child(Story::label("Inset")) .child( - ListItem::new("with_left_icon") + ListItem::new("hello_world") + .inset(true) + .start_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ) .child("Hello, world!") - .left_icon(Icon::Bell), + .end_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ), ) - .child(Story::label("With left avatar")) + .child(Story::label("With start slot icon")) + .child( + ListItem::new("with start slot_icon") + .child("Hello, world!") + .start_slot( + IconElement::new(Icon::Bell) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child(Story::label("With start slot avatar")) + .child( + ListItem::new("with_start slot avatar") + .child("Hello, world!") + .start_slot(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ))), + ) + .child(Story::label("With end slot")) .child( ListItem::new("with_left_avatar") .child("Hello, world!") - .left_avatar(SharedString::from( + .end_slot(Avatar::new(SharedString::from( "https://avatars.githubusercontent.com/u/1714999?v=4", - )), + ))), + ) + .child(Story::label("With end hover slot")) + .child( + ListItem::new("with_left_avatar") + .child("Hello, world!") + .end_slot( + h_stack() + .gap_2() + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))) + .child(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1789?v=4", + ))), + ) + .end_hover_slot(Avatar::new(SharedString::from( + "https://avatars.githubusercontent.com/u/1714999?v=4", + ))), ) .child(Story::label("With `on_click`")) .child( diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index ed81c2cd0a..3358968c72 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized { elevated(self, cx, ElevationIndex::ModalSurface) } + /// The theme's primary border color. + fn border_primary(self, cx: &mut WindowContext) -> Self { + self.border_color(cx.theme().colors().border) + } + + /// The theme's secondary or muted border color. + fn border_muted(self, cx: &mut WindowContext) -> Self { + self.border_color(cx.theme().colors().border_variant) + } + fn debug_bg_red(self) -> Self { - self.bg(gpui::red()) + self.bg(hsla(0. / 360., 1., 0.5, 1.)) } fn debug_bg_green(self) -> Self { - self.bg(gpui::green()) + self.bg(hsla(120. / 360., 1., 0.5, 1.)) } fn debug_bg_blue(self) -> Self { - self.bg(gpui::blue()) + self.bg(hsla(240. / 360., 1., 0.5, 1.)) } fn debug_bg_yellow(self) -> Self { From 985d4c7429986f5e6d06c7e26d89656716390c89 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 17:09:26 -0500 Subject: [PATCH 05/15] Remove TODO Thanks @ConradIrwin --- crates/feedback2/src/feedback_modal.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index e8715034c2..92ecd8d930 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -309,7 +309,6 @@ impl FeedbackModal { Ok(()) } - // TODO: Escape button calls dismiss fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { cx.emit(DismissEvent) } From aa55e55c7a3f886df7fab25b8d8ef1348a34b9f9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 17:25:07 -0500 Subject: [PATCH 06/15] Add config files for running Postgres inside Docker Compose (#3637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds config files for running the Postgres instance for local Zed development in a Docker Compose instance. For those of us who don't like to have a Postgres install always present on the host system 😄 Usage: ``` docker compose up -d ``` Release Notes: - N/A --- docker-compose.sql | 1 + docker-compose.yml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docker-compose.sql create mode 100644 docker-compose.yml diff --git a/docker-compose.sql b/docker-compose.sql new file mode 100644 index 0000000000..9cbd0bf0d1 --- /dev/null +++ b/docker-compose.sql @@ -0,0 +1 @@ +create database zed; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..78faf21a60 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.7" + +services: + postgres: + image: postgres:15 + container_name: zed_postgres + ports: + - 5432:5432 + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + - ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + postgres_data: From 1ad1cc114871061fc5a2aa01829e53c1efccaef2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Dec 2023 17:31:51 -0500 Subject: [PATCH 07/15] Fix variable name --- crates/feedback2/src/feedback_modal.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 92ecd8d930..e5c1ccdc9d 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -53,7 +53,7 @@ pub struct FeedbackModal { email_address_editor: View, awaiting_submission: bool, user_submitted: bool, - discarded: bool, + user_discarded: bool, character_count: i32, } @@ -71,7 +71,7 @@ impl ModalView for FeedbackModal { return true; } - if self.discarded { + if self.user_discarded { return true; } @@ -85,7 +85,7 @@ impl ModalView for FeedbackModal { cx.spawn(move |this, mut cx| async move { if answer.await.ok() == Some(0) { this.update(&mut cx, |this, cx| { - this.discarded = true; + this.user_discarded = true; cx.emit(DismissEvent) }) .log_err(); @@ -184,7 +184,7 @@ impl FeedbackModal { email_address_editor, awaiting_submission: false, user_submitted: false, - discarded: false, + user_discarded: false, character_count: 0, } } From d59de96921a4e0768c7590010c39f4cf391b6190 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 18:20:04 -0500 Subject: [PATCH 08/15] Style collab panel (#3638) This PR styles the collab panel. Release Notes: - N/A --------- Co-authored-by: Nate Butler Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/collab_ui2/src/collab_panel.rs | 269 ++++++++++---------- crates/ui2/src/components/disclosure.rs | 3 +- crates/ui2/src/components/list/list_item.rs | 16 +- script/storybook | 15 ++ 4 files changed, 167 insertions(+), 136 deletions(-) create mode 100755 script/storybook diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index a34d574957..0ad10f58de 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -176,11 +176,11 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json, - size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, - EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, - Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, - RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, + Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, + IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, + Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -402,7 +402,7 @@ impl CollabPanel { let filter_editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Filter channels, contacts", cx); + editor.set_placeholder_text("Filter...", cx); editor }); @@ -1157,24 +1157,20 @@ impl CollabPanel { ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) - .child( - h_stack() - .w_full() - .justify_between() - .child(Label::new(user.github_login.clone())) - .child(if is_pending { - Label::new("Calling").color(Color::Muted).into_any_element() - } else if is_current_user { - IconButton::new("leave-call", Icon::ArrowRight) - .on_click(cx.listener(move |this, _, cx| { - Self::leave_call(cx); - })) - .tooltip(|cx| Tooltip::text("Leave Call", cx)) - .into_any_element() - } else { - div().into_any_element() - }), - ) + .child(Label::new(user.github_login.clone())) + .end_slot(if is_pending { + Label::new("Calling").color(Color::Muted).into_any_element() + } else if is_current_user { + IconButton::new("leave-call", Icon::Exit) + .style(ButtonStyle::Subtle) + .on_click(cx.listener(move |this, _, cx| { + Self::leave_call(cx); + })) + .tooltip(|cx| Tooltip::text("Leave Call", cx)) + .into_any_element() + } else { + div().into_any_element() + }) .when_some(peer_id, |this, peer_id| { this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { @@ -1212,8 +1208,12 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .start_slot(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Folder)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Folder)), + ) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -1305,8 +1305,12 @@ impl CollabPanel { let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); ListItem::new(("screen", id)) - .start_slot(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Screen)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Screen)), + ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { this.on_click(cx.listener(move |this, _, cx| { @@ -1372,9 +1376,13 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.open_channel_notes(channel_id, cx); })) - .start_slot(render_tree_branch(false, cx)) - .child(IconButton::new(0, Icon::File)) - .child(Label::new("notes")) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::File)), + ) + .child(div().h_7().w_full().child(Label::new("notes"))) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } @@ -1387,8 +1395,12 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, cx| { this.join_channel_chat(channel_id, cx); })) - .start_slot(render_tree_branch(true, cx)) - .child(IconButton::new(0, Icon::MessageBubbles)) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::MessageBubbles)), + ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } @@ -2149,11 +2161,6 @@ impl CollabPanel { fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { v_stack() .size_full() - .child( - div() - .p_2() - .child(div().rounded(px(2.0)).child(self.filter_editor.clone())), - ) .child( v_stack() .size_full() @@ -2223,6 +2230,14 @@ impl CollabPanel { } })), ) + .child( + div().p_2().child( + div() + .border_primary(cx) + .border_t() + .child(self.filter_editor.clone()), + ), + ) } fn render_header( @@ -2274,22 +2289,32 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); - IconButton::new("channel-link", Icon::Copy) - .on_click(move |_, cx| { - let item = ClipboardItem::new(channel_link_copy.clone()); - cx.write_to_clipboard(item) - }) - .tooltip(|cx| Tooltip::text("Copy channel link", cx)) + div() + .invisible() + .group_hover("section-header", |this| this.visible()) + .child( + IconButton::new("channel-link", Icon::Copy) + .icon_size(IconSize::Small) + .size(ButtonSize::None) + .on_click(move |_, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) + }) + .tooltip(|cx| Tooltip::text("Copy channel link", cx)), + ) + .into_any_element() }), Section::Contacts => Some( IconButton::new("add-contact", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) - .tooltip(|cx| Tooltip::text("Search for new contact", cx)), + .tooltip(|cx| Tooltip::text("Search for new contact", cx)) + .into_any_element(), ), Section::Channels => Some( IconButton::new("add-channel", Icon::Plus) .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) - .tooltip(|cx| Tooltip::text("Create a channel", cx)), + .tooltip(|cx| Tooltip::text("Create a channel", cx)) + .into_any_element(), ), _ => None, }; @@ -2304,25 +2329,18 @@ impl CollabPanel { h_stack() .w_full() - .map(|el| { - if can_collapse { - el.child( - ListItem::new(text.clone()) - .child(div().w_full().child(Label::new(text))) - .selected(is_selected) - .toggle(Some(!is_collapsed)) - .on_click(cx.listener(move |this, _, cx| { - this.toggle_section_expanded(section, cx) - })), - ) - } else { - el.child( - ListHeader::new(text) - .when_some(button, |el, button| el.end_slot(button)) - .selected(is_selected), - ) - } - }) + .group("section-header") + .child( + ListHeader::new(text) + .toggle(if can_collapse { + Some(!is_collapsed) + } else { + None + }) + .inset(true) + .end_slot::(button) + .selected(is_selected), + ) .when(section == Section::Channels, |el| { el.drag_over::(|style| { style.bg(cx.theme().colors().ghost_element_hover) @@ -2460,7 +2478,7 @@ impl CollabPanel { .child(Label::new(github_login.clone())) .child(h_stack().children(controls)), ) - .start_slot::(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar))) + .start_slot(Avatar::new(user.avatar_uri.clone())) } fn render_contact_placeholder( @@ -2541,6 +2559,8 @@ impl CollabPanel { div() .id(channel_id as usize) .group("") + .flex() + .w_full() .on_drag({ let channel = channel.clone(); move |cx| { @@ -2566,71 +2586,10 @@ impl CollabPanel { ) .child( ListItem::new(channel_id as usize) - .indent_level(depth) + // Offset the indent depth by one to give us room to show the disclosure. + .indent_level(depth + 1) .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle - .start_slot( - IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) - .size(IconSize::Small) - .color(Color::Muted), - ) .selected(is_selected || is_active) - .child( - h_stack() - .w_full() - .justify_between() - .child( - h_stack() - .id(channel_id as usize) - .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render(cx))), - ) - .child( - h_stack() - .child( - div() - .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new( - "channel_chat", - Icon::MessageBubbles, - ) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx) - })) - .tooltip(|cx| { - Tooltip::text("Open channel chat", cx) - }), - ), - ) - .child( - div() - .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) - .child( - IconButton::new("channel_notes", Icon::File) - .icon_color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx) - })) - .tooltip(|cx| { - Tooltip::text("Open channel notes", cx) - }), - ), - ), - ), - ) .toggle(disclosed) .on_toggle( cx.listener(move |this, _, cx| { @@ -2650,7 +2609,57 @@ impl CollabPanel { move |this, event: &MouseDownEvent, cx| { this.deploy_channel_context_menu(event.position, channel_id, ix, cx) }, - )), + )) + .start_slot( + IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child( + h_stack() + .id(channel_id as usize) + .child(Label::new(channel.name.clone())) + .children(face_pile.map(|face_pile| face_pile.render(cx))), + ) + .end_slot( + h_stack() + .child( + div() + .id("channel_chat") + .when(!has_messages_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_chat", Icon::MessageBubbles) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), + ), + ) + .child( + div() + .id("channel_notes") + .when(!has_notes_notification, |el| el.invisible()) + .group_hover("", |style| style.visible()) + .child( + IconButton::new("channel_notes", Icon::File) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel notes", cx)), + ), + ), + ), ) .tooltip(|cx| Tooltip::text("Join channel", cx)) diff --git a/crates/ui2/src/components/disclosure.rs b/crates/ui2/src/components/disclosure.rs index 7d9a69bb3a..7d0f911d96 100644 --- a/crates/ui2/src/components/disclosure.rs +++ b/crates/ui2/src/components/disclosure.rs @@ -1,6 +1,7 @@ -use crate::{prelude::*, Color, Icon, IconButton, IconSize}; use gpui::ClickEvent; +use crate::{prelude::*, Color, Icon, IconButton, IconSize}; + #[derive(IntoElement)] pub struct Disclosure { is_open: bool, diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index df6e542816..403d3e7605 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -1,9 +1,10 @@ -use crate::{prelude::*, Disclosure}; use gpui::{ px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful, }; use smallvec::SmallVec; +use crate::{prelude::*, Disclosure}; + #[derive(IntoElement)] pub struct ListItem { id: ElementId, @@ -192,10 +193,15 @@ impl RenderOnce for ListItem { this.ml(self.indent_level as f32 * self.indent_step_size) } }) - .children( - self.toggle - .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), - ) + .children(self.toggle.map(|is_open| { + div() + .flex() + .absolute() + .left(rems(-1.)) + .invisible() + .group_hover("", |style| style.visible()) + .child(Disclosure::new(is_open).on_toggle(self.on_toggle)) + })) .child( h_stack() .flex_1() diff --git a/script/storybook b/script/storybook new file mode 100755 index 0000000000..bcabdef0af --- /dev/null +++ b/script/storybook @@ -0,0 +1,15 @@ +#!/bin/bash + +# This script takes a single text input and replaces 'list_item' with the input in a cargo run command + +# Check if an argument is provided +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign the argument to a variable +COMPONENT_NAME="$1" + +# Run the cargo command with the provided component name +cargo run -p storybook2 -- components/"$COMPONENT_NAME" From 137e4e9251937ce5268ab0f0b3391f1ef641d6e4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 19:12:20 -0500 Subject: [PATCH 09/15] Add `.visible_on_hover` helper method (#3639) This PR adds a `.visible_on_hover` helper method that can be used to make an element only visible on hover. I noticed we were repeating this similar stanza in a bunch of different spots: ```rs some_element .invisible() .group_hover("", |style| style.visible()) ``` so it seemed like a nice thing to factor out into a reusable utility. Release Notes: - N/A --- crates/collab_ui2/src/chat_panel.rs | 8 ++--- crates/collab_ui2/src/collab_panel.rs | 35 ++++++++----------- crates/editor2/src/editor.rs | 3 +- crates/ui2/src/components/list/list_header.rs | 3 +- crates/ui2/src/components/list/list_item.rs | 6 ++-- crates/ui2/src/components/tab.rs | 3 +- crates/ui2/src/prelude.rs | 1 + crates/ui2/src/ui2.rs | 2 ++ crates/ui2/src/visible_on_hover.rs | 13 +++++++ 9 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 crates/ui2/src/visible_on_hover.rs diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 587efbe95f..9096770166 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore}; use std::sync::Arc; use theme::ActiveTheme as _; use time::{OffsetDateTime, UtcOffset}; -use ui::{ - h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon, - IconButton, Label, Tooltip, -}; +use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -382,12 +379,11 @@ impl ChatPanel { .child(text.element("body".into(), cx)) .child( div() - .invisible() .absolute() .top_1() .right_2() .w_8() - .group_hover("", |this| this.visible()) + .visible_on_hover("") .child(render_remove(message_id_to_remove, cx)), ) .into_any() diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 0ad10f58de..cf1ac5205a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2290,8 +2290,7 @@ impl CollabPanel { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); div() - .invisible() - .group_hover("section-header", |this| this.visible()) + .visible_on_hover("section-header") .child( IconButton::new("channel-link", Icon::Copy) .icon_size(IconSize::Small) @@ -2381,21 +2380,17 @@ impl CollabPanel { }) .when(!calling, |el| { el.child( - div() - .id("remove_contact") - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("remove_contact", Icon::Close) - .icon_color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener({ - let github_login = github_login.clone(); - move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - } - })), - ), + div().visible_on_hover("").child( + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), ) }), ) @@ -2626,8 +2621,7 @@ impl CollabPanel { .child( div() .id("channel_chat") - .when(!has_messages_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) + .when(!has_messages_notification, |el| el.visible_on_hover("")) .child( IconButton::new("channel_chat", Icon::MessageBubbles) .icon_color(if has_messages_notification { @@ -2644,8 +2638,7 @@ impl CollabPanel { .child( div() .id("channel_notes") - .when(!has_notes_notification, |el| el.invisible()) - .group_hover("", |style| style.visible()) + .when(!has_notes_notification, |el| el.visible_on_hover("")) .child( IconButton::new("channel_notes", Icon::File) .icon_color(if has_notes_notification { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0d19b53d29..aba8dbd4d4 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9766,8 +9766,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend div() .border() .border_color(gpui::red()) - .invisible() - .group_hover(group_id, |style| style.visible()) + .visible_on_hover(group_id) .child( IconButton::new(copy_id.clone(), Icon::Copy) .icon_color(Color::Muted) diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 6c497752ae..d082574a92 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -111,10 +111,9 @@ impl RenderOnce for ListHeader { .when_some(self.end_hover_slot, |this, end_hover_slot| { this.child( div() - .invisible() - .group_hover("list_header", |this| this.visible()) .absolute() .right_0() + .visible_on_hover("list_header") .child(end_hover_slot), ) }), diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 403d3e7605..44b7f33c38 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -198,8 +198,7 @@ impl RenderOnce for ListItem { .flex() .absolute() .left(rems(-1.)) - .invisible() - .group_hover("", |style| style.visible()) + .visible_on_hover("") .child(Disclosure::new(is_open).on_toggle(self.on_toggle)) })) .child( @@ -226,8 +225,7 @@ impl RenderOnce for ListItem { .absolute() .right_2() .top_0() - .invisible() - .group_hover("list_item", |this| this.visible()) + .visible_on_hover("list_item") .child(end_hover_slot), ) }), diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index be1ce8dd12..8114a322e3 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -158,7 +158,6 @@ impl RenderOnce for Tab { ) .child( h_stack() - .invisible() .w_3() .h_3() .justify_center() @@ -167,7 +166,7 @@ impl RenderOnce for Tab { TabCloseSide::Start => this.left_1(), TabCloseSide::End => this.right_1(), }) - .group_hover("", |style| style.visible()) + .visible_on_hover("") .children(self.end_slot), ) .children(self.children), diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 076d34644c..a0a0adeb1d 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -9,6 +9,7 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::visible_on_hover::*; pub use crate::{h_stack, v_stack}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton}; pub use crate::{ButtonCommon, Color, StyledExt}; diff --git a/crates/ui2/src/ui2.rs b/crates/ui2/src/ui2.rs index 6c5669741b..5c79199100 100644 --- a/crates/ui2/src/ui2.rs +++ b/crates/ui2/src/ui2.rs @@ -21,6 +21,7 @@ mod selectable; mod styled_ext; mod styles; pub mod utils; +mod visible_on_hover; pub use clickable::*; pub use components::*; @@ -30,3 +31,4 @@ pub use prelude::*; pub use selectable::*; pub use styled_ext::*; pub use styles::*; +pub use visible_on_hover::*; diff --git a/crates/ui2/src/visible_on_hover.rs b/crates/ui2/src/visible_on_hover.rs new file mode 100644 index 0000000000..dfab5ab3e6 --- /dev/null +++ b/crates/ui2/src/visible_on_hover.rs @@ -0,0 +1,13 @@ +use gpui::{InteractiveElement, SharedString, Styled}; + +pub trait VisibleOnHover: InteractiveElement + Styled + Sized { + /// Sets the element to only be visible when the specified group is hovered. + /// + /// Pass `""` as the `group_name` to use the global group. + fn visible_on_hover(self, group_name: impl Into) -> Self { + self.invisible() + .group_hover(group_name, |style| style.visible()) + } +} + +impl VisibleOnHover for E {} From 93029376d977cfc262ce42d2e87432dc37a7e4b2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Dec 2023 16:52:11 -0800 Subject: [PATCH 10/15] Start work on allowing dragging tabs onto panes and pane edges --- crates/workspace2/src/pane.rs | 76 ++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index bcbadc4e53..275f78dd9f 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1759,6 +1759,33 @@ impl Pane { }) .log_err(); } + + fn handle_split_tab_drop( + &mut self, + dragged_tab: &View, + split_direction: SplitDirection, + cx: &mut ViewContext<'_, Pane>, + ) { + let dragged_tab = dragged_tab.read(cx); + let item_id = dragged_tab.item_id; + let from_pane = dragged_tab.pane.clone(); + let to_pane = cx.view().clone(); + self.workspace + .update(cx, |workspace, cx| { + cx.defer(move |workspace, cx| { + let item = from_pane + .read(cx) + .items() + .find(|item| item.item_id() == item_id) + .map(|i| i.boxed_clone()); + + if let Some(item) = item { + workspace.split_item(split_direction, item, cx); + } + }); + }) + .log_err(); + } } impl FocusableView for Pane { @@ -1852,7 +1879,54 @@ impl Render for Pane { .child(self.render_tab_bar(cx)) .child(self.toolbar.clone()) .child(if let Some(item) = self.active_item() { - div().flex().flex_1().child(item.to_any()) + let mut drag_target_color = cx.theme().colors().text; + drag_target_color.a = 0.5; + + div() + .flex() + .flex_1() + .relative() + .child(item.to_any()) + .child( + div() + .absolute() + .full() + .z_index(1) + .drag_over::(|style| style.bg(drag_target_color)) + .on_drop(cx.listener( + move |this, dragged_tab: &View, cx| { + this.handle_tab_drop(dragged_tab, this.active_item_index(), cx) + }, + )), + ) + .children( + [ + (SplitDirection::Up, 2), + (SplitDirection::Down, 2), + (SplitDirection::Left, 3), + (SplitDirection::Right, 3), + ] + .into_iter() + .map(|(direction, z_index)| { + let div = div() + .absolute() + .z_index(z_index) + .invisible() + .bg(drag_target_color) + .drag_over::(|style| style.visible()) + .on_drop(cx.listener( + move |this, dragged_tab: &View, cx| { + this.handle_split_tab_drop(dragged_tab, direction, cx) + }, + )); + match direction { + SplitDirection::Up => div.top_0().left_0().right_0().h_32(), + SplitDirection::Down => div.left_0().bottom_0().right_0().h_32(), + SplitDirection::Left => div.top_0().left_0().bottom_0().w_32(), + SplitDirection::Right => div.top_0().bottom_0().right_0().w_32(), + } + }), + ) } else { h_stack() .items_center() From 9059d7015369f21f6060f23eb64c21bff439ffbd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Dec 2023 17:07:23 -0800 Subject: [PATCH 11/15] Ensure only top layer is styled with drag over style --- crates/gpui2/src/elements/div.rs | 118 ++++++++++++++++--------------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a102c71a6f..dfbb5aff21 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -740,7 +740,7 @@ impl Interactivity { if style .background .as_ref() - .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent())) + .is_some_and(|fill| fill.color().is_some()) { cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds)) } @@ -1120,78 +1120,84 @@ impl Interactivity { let mut style = Style::default(); style.refine(&self.base_style); - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if let Some(in_focus_style) = self.in_focus_style.as_ref() { - if focus_handle.within_focused(cx) { - style.refine(in_focus_style); + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if let Some(in_focus_style) = self.in_focus_style.as_ref() { + if focus_handle.within_focused(cx) { + style.refine(in_focus_style); + } } - } - if let Some(focus_style) = self.focus_style.as_ref() { - if focus_handle.is_focused(cx) { - style.refine(focus_style); - } - } - } - - if let Some(bounds) = bounds { - let mouse_position = cx.mouse_position(); - if let Some(group_hover) = self.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) - { - style.refine(&group_hover.style); + if let Some(focus_style) = self.focus_style.as_ref() { + if focus_handle.is_focused(cx) { + style.refine(focus_style); } } } - if let Some(hover_style) = self.hover_style.as_ref() { - if bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) - && cx.was_top_layer(&mouse_position, cx.stacking_order()) - { - style.refine(hover_style); - } - } - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains(&mouse_position) + if let Some(bounds) = bounds { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) { - style.refine(&group_drag_style.style); + style.refine(&group_hover.style); } } } - - for (state_type, drag_over_style) in &self.drag_over_styles { - if *state_type == drag.view.entity_type() - && bounds - .intersect(&cx.content_mask().bounds) - .contains(&mouse_position) + if let Some(hover_style) = self.hover_style.as_ref() { + if bounds + .intersect(&cx.content_mask().bounds) + .contains(&mouse_position) + && cx.was_top_layer(&mouse_position, cx.stacking_order()) { - style.refine(drag_over_style); + style.refine(hover_style); } } - cx.active_drag = Some(drag); - } - } + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.view.entity_type() + && group_bounds.contains(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } - let clicked_state = element_state.clicked_state.borrow(); - if clicked_state.group { - if let Some(group) = self.group_active_style.as_ref() { - style.refine(&group.style) - } - } + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.view.entity_type() + && bounds + .intersect(&cx.content_mask().bounds) + .contains(&mouse_position) + && cx.was_top_layer_under_active_drag( + &mouse_position, + cx.stacking_order(), + ) + { + style.refine(drag_over_style); + } + } - if let Some(active_style) = self.active_style.as_ref() { - if clicked_state.element { - style.refine(active_style) + cx.active_drag = Some(drag); + } } - } + + let clicked_state = element_state.clicked_state.borrow(); + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if let Some(active_style) = self.active_style.as_ref() { + if clicked_state.element { + style.refine(active_style) + } + } + }); style } From 4f32f662711a6885e1a14ab7d17317dc36868d23 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Dec 2023 17:14:16 -0800 Subject: [PATCH 12/15] Clone item when dragging to split --- crates/workspace2/src/pane.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 275f78dd9f..a55469fbad 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1777,10 +1777,11 @@ impl Pane { .read(cx) .items() .find(|item| item.item_id() == item_id) - .map(|i| i.boxed_clone()); - + .map(|item| item.boxed_clone()); if let Some(item) = item { - workspace.split_item(split_direction, item, cx); + if let Some(item) = item.clone_on_split(workspace.database_id(), cx) { + workspace.split_item(split_direction, item, cx); + } } }); }) From 057b235c564cf8d5c7e87314f19247eccbfb241e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 20:42:27 -0500 Subject: [PATCH 13/15] Implement `VisibleOnHover` for `IconButton` (#3642) This PR implements the `VisibleOnHover` trait for `IconButton`s. I noticed that in a lot of places we were wrapping an `IconButton` in an extra `div` just so we could call `visible_on_hover` on it. By implementing the trait on `IconButton` directly it allows us to avoid the interstitial `div` entirely. Release Notes: - N/A --- crates/collab_ui2/src/chat_panel.rs | 20 ++-- crates/collab_ui2/src/collab_panel.rs | 94 +++++++++---------- crates/editor2/src/editor.rs | 21 ++--- .../ui2/src/components/button/button_like.rs | 13 ++- .../ui2/src/components/button/icon_button.rs | 7 ++ crates/ui2/src/visible_on_hover.rs | 8 +- 6 files changed, 81 insertions(+), 82 deletions(-) diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 9096770166..f3f2a37171 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -384,7 +384,13 @@ impl ChatPanel { .right_2() .w_8() .visible_on_hover("") - .child(render_remove(message_id_to_remove, cx)), + .children(message_id_to_remove.map(|message_id| { + IconButton::new(("remove", message_id), Icon::XCircle).on_click( + cx.listener(move |this, _, cx| { + this.remove_message(message_id, cx); + }), + ) + })), ) .into_any() } @@ -524,18 +530,6 @@ impl ChatPanel { } } -fn render_remove(message_id_to_remove: Option, cx: &mut ViewContext) -> AnyElement { - if let Some(message_id) = message_id_to_remove { - IconButton::new(("remove", message_id), Icon::XCircle) - .on_click(cx.listener(move |this, _, cx| { - this.remove_message(message_id, cx); - })) - .into_any_element() - } else { - div().into_any_element() - } -} - impl EventEmitter for ChatPanel {} impl Render for ChatPanel { diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index cf1ac5205a..95ca7cfd25 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2289,18 +2289,15 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); - div() + IconButton::new("channel-link", Icon::Copy) + .icon_size(IconSize::Small) + .size(ButtonSize::None) .visible_on_hover("section-header") - .child( - IconButton::new("channel-link", Icon::Copy) - .icon_size(IconSize::Small) - .size(ButtonSize::None) - .on_click(move |_, cx| { - let item = ClipboardItem::new(channel_link_copy.clone()); - cx.write_to_clipboard(item) - }) - .tooltip(|cx| Tooltip::text("Copy channel link", cx)), - ) + .on_click(move |_, cx| { + let item = ClipboardItem::new(channel_link_copy.clone()); + cx.write_to_clipboard(item) + }) + .tooltip(|cx| Tooltip::text("Copy channel link", cx)) .into_any_element() }), Section::Contacts => Some( @@ -2380,17 +2377,16 @@ impl CollabPanel { }) .when(!calling, |el| { el.child( - div().visible_on_hover("").child( - IconButton::new("remove_contact", Icon::Close) - .icon_color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener({ - let github_login = github_login.clone(); - move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - } - })), - ), + IconButton::new("remove_contact", Icon::Close) + .icon_color(Color::Muted) + .visible_on_hover("") + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), ) }), ) @@ -2619,38 +2615,32 @@ impl CollabPanel { .end_slot( h_stack() .child( - div() - .id("channel_chat") - .when(!has_messages_notification, |el| el.visible_on_hover("")) - .child( - IconButton::new("channel_chat", Icon::MessageBubbles) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.join_channel_chat(channel_id, cx) - })) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)), - ), + IconButton::new("channel_chat", Icon::MessageBubbles) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }) + .when(!has_messages_notification, |this| { + this.visible_on_hover("") + }) + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)), ) .child( - div() - .id("channel_notes") - .when(!has_notes_notification, |el| el.visible_on_hover("")) - .child( - IconButton::new("channel_notes", Icon::File) - .icon_color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| { - this.open_channel_notes(channel_id, cx) - })) - .tooltip(|cx| Tooltip::text("Open channel notes", cx)), - ), + IconButton::new("channel_notes", Icon::File) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .when(!has_notes_notification, |this| this.visible_on_hover("")) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel notes", cx)), ), ), ) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index aba8dbd4d4..89b5fd2efb 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9763,18 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend .px_1p5() .child(HighlightedLabel::new(line.clone(), highlights.clone())) .child( - div() - .border() - .border_color(gpui::red()) - .visible_on_hover(group_id) - .child( - IconButton::new(copy_id.clone(), Icon::Copy) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .style(ButtonStyle::Transparent) - .on_click(cx.listener(move |_, _, cx| write_to_clipboard)) - .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), - ), + div().border().border_color(gpui::red()).child( + IconButton::new(copy_id.clone(), Icon::Copy) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .style(ButtonStyle::Transparent) + .visible_on_hover(group_id) + .on_click(cx.listener(move |_, _, cx| write_to_clipboard)) + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), + ), ) })) .into_any_element() diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 7203b3494f..8255490476 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength}; use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; use smallvec::SmallVec; -use crate::h_stack; use crate::prelude::*; pub trait ButtonCommon: Clickable + Disableable { @@ -250,6 +249,7 @@ impl ButtonSize { /// This is also used to build the prebuilt buttons. #[derive(IntoElement)] pub struct ButtonLike { + base: Div, id: ElementId, pub(super) style: ButtonStyle, pub(super) disabled: bool, @@ -264,6 +264,7 @@ pub struct ButtonLike { impl ButtonLike { pub fn new(id: impl Into) -> Self { Self { + base: div(), id: id.into(), style: ButtonStyle::default(), disabled: false, @@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike { } } +impl VisibleOnHover for ButtonLike { + fn visible_on_hover(mut self, group_name: impl Into) -> Self { + self.base = self.base.visible_on_hover(group_name); + self + } +} + impl ParentElement for ButtonLike { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children @@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike { type Rendered = Stateful
; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - h_stack() + self.base + .h_flex() .id(self.id.clone()) .group("") .flex_none() diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index f49120e90c..3a53bb6cb0 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -98,6 +98,13 @@ impl ButtonCommon for IconButton { } } +impl VisibleOnHover for IconButton { + fn visible_on_hover(mut self, group_name: impl Into) -> Self { + self.base = self.base.visible_on_hover(group_name); + self + } +} + impl RenderOnce for IconButton { type Rendered = ButtonLike; diff --git a/crates/ui2/src/visible_on_hover.rs b/crates/ui2/src/visible_on_hover.rs index dfab5ab3e6..aefa7ac10c 100644 --- a/crates/ui2/src/visible_on_hover.rs +++ b/crates/ui2/src/visible_on_hover.rs @@ -1,13 +1,15 @@ use gpui::{InteractiveElement, SharedString, Styled}; -pub trait VisibleOnHover: InteractiveElement + Styled + Sized { +pub trait VisibleOnHover { /// Sets the element to only be visible when the specified group is hovered. /// /// Pass `""` as the `group_name` to use the global group. + fn visible_on_hover(self, group_name: impl Into) -> Self; +} + +impl VisibleOnHover for E { fn visible_on_hover(self, group_name: impl Into) -> Self { self.invisible() .group_hover(group_name, |style| style.visible()) } } - -impl VisibleOnHover for E {} From 474f09ca3f443353455a9fb58d55dc2a7dffae6b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 21:03:50 -0500 Subject: [PATCH 14/15] Remove unneeded left-click filtering in `ListItem` (#3643) This PR removes the left-click filtering from the `on_click` handler for `ListItem`s. It's no longer needed after #3584. Release Notes: - N/A --- crates/ui2/src/components/list/list_item.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 44b7f33c38..8806112ded 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -171,13 +171,7 @@ impl RenderOnce for ListItem { }) }) .when_some(self.on_click, |this, on_click| { - this.cursor_pointer().on_click(move |event, cx| { - // HACK: GPUI currently fires `on_click` with any mouse button, - // but we only care about the left button. - if event.down.button == MouseButton::Left { - (on_click)(event, cx) - } - }) + this.cursor_pointer().on_click(on_click) }) .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { this.on_mouse_down(MouseButton::Right, move |event, cx| { From ceede28fabef6e1e54284ee9cdd056feef082144 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 13 Dec 2023 21:14:21 -0500 Subject: [PATCH 15/15] Ensure the outer `ListItem` element has a unique ID (#3644) This PR fixes an issue where the outer `ListItem` element was using a static ID instead of the one provided to the component. Now that active states are fixed, this meant that any time there were sibling list items they would share active states if one of them was clicked. Release Notes: - N/A --- crates/ui2/src/components/list/list_item.rs | 4 ++-- crates/ui2/src/components/stories/list_item.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 8806112ded..481d96d242 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -128,7 +128,7 @@ impl RenderOnce for ListItem { fn render(self, cx: &mut WindowContext) -> Self::Rendered { h_stack() - .id("item_container") + .id(self.id) .w_full() .relative() // When an item is inset draw the indent spacing outside of the item @@ -151,7 +151,7 @@ impl RenderOnce for ListItem { }) .child( h_stack() - .id(self.id) + .id("inner_list_item") .w_full() .relative() .gap_1() diff --git a/crates/ui2/src/components/stories/list_item.rs b/crates/ui2/src/components/stories/list_item.rs index fbcea44b57..b070be663e 100644 --- a/crates/ui2/src/components/stories/list_item.rs +++ b/crates/ui2/src/components/stories/list_item.rs @@ -17,7 +17,7 @@ impl Render for ListItemStory { .child(ListItem::new("hello_world").child("Hello, world!")) .child(Story::label("Inset")) .child( - ListItem::new("hello_world") + ListItem::new("inset_list_item") .inset(true) .start_slot( IconElement::new(Icon::Bell) @@ -59,7 +59,7 @@ impl Render for ListItemStory { ) .child(Story::label("With end hover slot")) .child( - ListItem::new("with_left_avatar") + ListItem::new("with_end_hover_slot") .child("Hello, world!") .end_slot( h_stack()