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 ca0a50546e0f56b80ca8b6c055fde0ab57f430f9..fd63b49a1a6b7a47054d700975d7d79f29847698 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 033ff8b69c542ea96e5d3cac6d7d8320c4383946..b29c28c6eb7d09ee047f3f4b60b974f26b9ebe98 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 2190066df595a7082dec1beba6a0ba2d5e6454ab..95cf2ae78c7cb31909e841c6ee54f769f5cd60e7 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 3c4d21ad50b0a6c7bd7959a615cf9336adf458f9..e94702ff9c08c8239451b49ec784e93cf3d71366 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 db5eebff5389a1f3bd9f0561a017fcb109ec904e..6543eb7213e42d26247d1e988f385dbe7fae4853 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 0000000000000000000000000000000000000000..e11dd373a80898691eba163161f530de9d1108e9 --- /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 0000000000000000000000000000000000000000..da67fc888f43deed6e470940a8deaaaf22030b16 --- /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 859afee4f7017826e84b9fb7b5fc3ddd7d3eefff..6646eb5ffc3b47141f1bac1df6a7c4f6658f7f8c 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 bbb8382cb21714a483be04dec2339d2d0227a402..ca8cd7a2a2a1d608c5c0821aa748de5539c47cd7 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 fd63b49a1a6b7a47054d700975d7d79f29847698..68c068dfe98473872024097d3ca1745866fc66eb 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 ac7457abe087954b918e6253428223837b0ca828..a34d574957e05ea2be5ffe6e446f2b588178d391 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 db5eebff5389a1f3bd9f0561a017fcb109ec904e..8d80f4b36c2d22783c4cd8921f0c21dc0b0d8470 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 3e542985147ddb99069f17b4922d0eb99fa2956e..250272b19882f031ab7af438bf3cfea7906bcec8 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 933a1a95d7f00712e3fe640c4c8ebd597141978c..6c497752aeff5f58f2e60d209737fb0bd8d823b4 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 28a8b8cecbf159cdb02ef44a4e2ea74ae14ca71d..df6e542816eaba0526f3551729e42cc50ce846be 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 0398a110e977f94dd75bf458f7ef879c7a1a3cfd..346b13ddaa3d7f672813724022a323f29c6b720a 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 17f07b7b0bf90712dc9b8703c7f51261f5e5c49f..07a99dabe5bbb29bd25bde471948b709350123dc 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 056eaa276210c09cfbda303bf682db02562854cf..3c80afdde382be760b213531628a6774b43c5302 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 91e95348fd936e08060eaf4a24989f4caef56462..fbcea44b579508a15b99dd746144e572e7c50de8 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!") + .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!") - .left_avatar(SharedString::from( + .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 ed81c2cd0a78a0f353ba66dcd563e445e9abdbbd..3358968c727c26872a6140b83a68b81043634c52 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 e8715034c2635d8bb1d70dc64c22c5cc9847977e..92ecd8d930d4695e9cd24f6f60722541a4ddb769 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 0000000000000000000000000000000000000000..9cbd0bf0d1e762ecc82b590eee6f10a46f31569f --- /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 0000000000000000000000000000000000000000..78faf21a604ead928478e13ba0fd1c8ec5c878e8 --- /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 92ecd8d930d4695e9cd24f6f60722541a4ddb769..e5c1ccdc9d12b80e91fd73fc9cb9ce7fd4dc902c 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 a34d574957e05ea2be5ffe6e446f2b588178d391..0ad10f58de00c7878553585c4236b4ef3df2d54a 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 7d9a69bb3a356ea37482db5ec7b2856efd680461..7d0f911d9689372971585f6d76148ffa35ed1b59 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 df6e542816eaba0526f3551729e42cc50ce846be..403d3e760591294e78f5d22393464008f32e6c2e 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 0000000000000000000000000000000000000000..bcabdef0afa8f52e55673eeb652014edcae440da --- /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 587efbe95f272478439560a24c8b6421b88bd004..9096770166f40eb9da465612a6e408ac9b350c7f 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 0ad10f58de00c7878553585c4236b4ef3df2d54a..cf1ac5205a170c631289ab503fca8871bdbe2752 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 0d19b53d29fbe71e6e893ed6b2953bd136dd1986..aba8dbd4d4292173e0cdd3e372e10f63d3f0b629 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 6c497752aeff5f58f2e60d209737fb0bd8d823b4..d082574a92fbdc36cc09d132c192b5f018920042 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 403d3e760591294e78f5d22393464008f32e6c2e..44b7f33c38a873344d40afc5fa12fadc78418ff5 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 be1ce8dd12e9df16d810a31f6bc6cb15606749ca..8114a322e300cac465981438bf8ad02d84ca029e 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 076d34644c931a0a930efd51d1ff88d01d7af4d7..a0a0adeb1db723d1729471e8dff51f4979f79a87 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 6c5669741be05d2bdbf895d991c5e6a8f301e02f..5c79199100ed26155cc83f684c32139f3a1ebf54 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 0000000000000000000000000000000000000000..dfab5ab3e6476fa39600ad4a9f405daeb93ea7bc --- /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 bcbadc4e532bcde153d4d70fc7044d51a490d44d..275f78dd9f2fc0c3b8165ff92b80200c46298da0 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 | 110 ++++++++++++++++--------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a102c71a6fbc1d8d0dbc0b2d984efcbdbc677276..dfbb5aff21bc10ff419cd311480dc592b8fbc077 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(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) + 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(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(&group_hover.style); + style.refine(hover_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 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); + } + } + } + + for (state_type, drag_over_style) in &self.drag_over_styles { if *state_type == drag.view.entity_type() - && group_bounds.contains(&mouse_position) + && bounds + .intersect(&cx.content_mask().bounds) + .contains(&mouse_position) + && cx.was_top_layer_under_active_drag( + &mouse_position, + cx.stacking_order(), + ) { - style.refine(&group_drag_style.style); + style.refine(drag_over_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) - { - style.refine(drag_over_style); - } + cx.active_drag = Some(drag); } - - 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) + 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) + 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 275f78dd9f2fc0c3b8165ff92b80200c46298da0..a55469fbadd096bc3a041b3ebb3cc12614be871d 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 9096770166f40eb9da465612a6e408ac9b350c7f..f3f2a37171b32c5be5c610725951b199b1a28525 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 cf1ac5205a170c631289ab503fca8871bdbe2752..95ca7cfd2545180fad9135a37fb80526e53ec030 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 aba8dbd4d4292173e0cdd3e372e10f63d3f0b629..89b5fd2efb91ff43f4683b0923f5658888ecc72a 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 7203b3494f1ce0608b5e8528e5d136932d9b63f9..8255490476b8a58f4a39d03de10412151f320db6 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 f49120e90c1c1afea3857152ee6d286cb7051596..3a53bb6cb0178ffb3046d385debd3c0ecb35d23d 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 dfab5ab3e6476fa39600ad4a9f405daeb93ea7bc..aefa7ac10c50646edca24f6ff2219418a44c8be9 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 44b7f33c38a873344d40afc5fa12fadc78418ff5..8806112dedc9e65693c5afff350fd107f7c3e1b8 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 8806112dedc9e65693c5afff350fd107f7c3e1b8..481d96d242e81d78e656be158f555a4077a1ee89 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 fbcea44b579508a15b99dd746144e572e7c50de8..b070be663e8920938c1bbbc43988585cb3d07384 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()