diff --git a/Cargo.lock b/Cargo.lock index f6c9639f99a37659f6a92b42b6b4bcaf67b51a37..805a9f4bf606a51f470862629d4124921175e6d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1880,6 +1880,30 @@ dependencies = [ "zed-actions", ] +[[package]] +name = "command_palette2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "picker2", + "project2", + "serde", + "serde_json", + "settings2", + "theme2", + "ui2", + "util", + "workspace2", + "zed_actions2", +] + [[package]] name = "component_test" version = "0.1.0" @@ -6135,6 +6159,7 @@ dependencies = [ "serde_json", "settings2", "theme2", + "ui2", "util", ] @@ -11371,6 +11396,7 @@ dependencies = [ "cli", "client2", "collections", + "command_palette2", "copilot2", "ctor", "db2", @@ -11457,6 +11483,15 @@ dependencies = [ "util", "uuid 1.4.1", "workspace2", + "zed_actions2", +] + +[[package]] +name = "zed_actions2" +version = "0.1.0" +dependencies = [ + "gpui2", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1b8081d06639c8ff611e2b5228cbfee1e6005b4b..905750f8352b02422fa0815b7a51e17d74b0daff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/command_palette2", "crates/component_test", "crates/context_menu", "crates/copilot", @@ -110,7 +111,8 @@ members = [ "crates/xtask", "crates/zed", "crates/zed2", - "crates/zed-actions" + "crates/zed-actions", + "crates/zed_actions2" ] default-members = ["crates/zed"] resolver = "2" diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bcc0099c2086f03fac0f13cd6541071fe91fb8f8 --- /dev/null +++ b/crates/command_palette2/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "command_palette2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/command_palette.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +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" } +settings = { package = "settings2", path = "../settings2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package="workspace2", path = "../workspace2" } +zed_actions = { package = "zed_actions2", path = "../zed_actions2" } +anyhow.workspace = true +serde.workspace = true +[dev-dependencies] +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +language = { package="language2", path = "../language2", features = ["test-support"] } +project = { package="project2", path = "../project2", features = ["test-support"] } +serde_json.workspace = true +workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] } +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..abba09519b1685a627fb4701468fa1b644761bc2 --- /dev/null +++ b/crates/command_palette2/src/command_palette.rs @@ -0,0 +1,534 @@ +use collections::{CommandPaletteFilter, HashMap}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, + ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, + WeakView, WindowContext, +}; +use picker::{Picker, PickerDelegate}; +use std::cmp::{self, Reverse}; +use theme::ActiveTheme; +use ui::{modal, Label}; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, + ResultExt, +}; +use workspace::{Modal, ModalEvent, Workspace}; +use zed_actions::OpenZedURL; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.set_global(HitCounts::default()); + cx.observe_new_views(CommandPalette::register).detach(); +} + +pub struct CommandPalette { + picker: View>, +} + +impl CommandPalette { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return; + }; + workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); + }); + } + + fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + let filter = cx.try_global::(); + + let commands = cx + .available_actions() + .into_iter() + .filter_map(|action| { + let name = action.name(); + let namespace = name.split("::").next().unwrap_or("malformed action name"); + if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + return None; + } + + Some(Command { + name: humanize_action_name(&name), + action, + keystrokes: vec![], // todo!() + }) + }) + .collect(); + + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +impl EventEmitter for CommandPalette {} +impl Modal for CommandPalette { + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| picker.focus(cx)); + } +} + +impl Render for CommandPalette { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + modal(cx).w_96().child(self.picker.clone()) + } +} + +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + +pub struct CommandPaletteDelegate { + command_palette: WeakView, + commands: Vec, + matches: Vec, + selected_ix: usize, + previous_focus_handle: FocusHandle, +} + +struct Command { + name: String, + action: Box, + keystrokes: Vec, +} + +impl Clone for Command { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + } + } +} +/// Hit count for each command in the palette. +/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because +/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. +#[derive(Default)] +struct HitCounts(HashMap); + +impl CommandPaletteDelegate { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + ) -> Self { + Self { + command_palette, + matches: commands + .iter() + .enumerate() + .map(|(i, command)| StringMatch { + candidate_id: i, + string: command.name.clone(), + positions: Vec::new(), + score: 0.0, + }) + .collect(), + commands, + selected_ix: 0, + previous_focus_handle, + } + } +} + +impl PickerDelegate for CommandPaletteDelegate { + type ListItem = Div>; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_ix + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_ix = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let mut commands = self.commands.clone(); + + cx.spawn(move |picker, mut cx| async move { + cx.read_global::(|hit_counts, _| { + commands.sort_by_key(|action| { + ( + Reverse(hit_counts.0.get(&action.name).cloned()), + action.name.clone(), + ) + }); + }) + .ok(); + + let candidates = commands + .iter() + .enumerate() + .map(|(ix, command)| StringMatchCandidate { + id: ix, + string: command.name.to_string(), + char_bag: command.name.chars().collect(), + }) + .collect::>(); + let mut matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background_executor().clone(), + ) + .await + }; + + let mut intercept_result = cx + .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { + (interceptor)(&query, cx) + }) + .flatten(); + + if *RELEASE_CHANNEL == ReleaseChannel::Dev { + if parse_zed_link(&query).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedURL { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + } + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + { + matches.remove(idx); + } + commands.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } + picker + .update(&mut cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.commands = commands; + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_ix = 0; + } else { + delegate.selected_ix = + cmp::min(delegate.selected_ix, delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.command_palette + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if self.matches.is_empty() { + self.dismissed(cx); + return; + } + let action_ix = self.matches[self.selected_ix].candidate_id; + let command = self.commands.swap_remove(action_ix); + cx.update_global(|hit_counts: &mut HitCounts, _| { + *hit_counts.0.entry(command.name).or_default() += 1; + }); + let action = command.action; + cx.focus(&self.previous_focus_handle); + cx.dispatch_action(action); + self.dismissed(cx); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem { + let colors = cx.theme().colors(); + let Some(command) = self + .matches + .get(ix) + .and_then(|m| self.commands.get(m.candidate_id)) + else { + return div(); + }; + + div() + .text_color(colors.text) + .when(selected, |s| { + s.border_l_10().border_color(colors.terminal_ansi_yellow) + }) + .hover(|style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .child(Label::new(command.name.clone())) + } + + // fn render_match( + // &self, + // ix: usize, + // mouse_state: &mut MouseState, + // selected: bool, + // cx: &gpui::AppContext, + // ) -> AnyElement> { + // let mat = &self.matches[ix]; + // let command = &self.actions[mat.candidate_id]; + // let theme = theme::current(cx); + // let style = theme.picker.item.in_state(selected).style_for(mouse_state); + // let key_style = &theme.command_palette.key.in_state(selected); + // let keystroke_spacing = theme.command_palette.keystroke_spacing; + + // Flex::row() + // .with_child( + // Label::new(mat.string.clone(), style.label.clone()) + // .with_highlights(mat.positions.clone()), + // ) + // .with_children(command.keystrokes.iter().map(|keystroke| { + // Flex::row() + // .with_children( + // [ + // (keystroke.ctrl, "^"), + // (keystroke.alt, "⌥"), + // (keystroke.cmd, "⌘"), + // (keystroke.shift, "⇧"), + // ] + // .into_iter() + // .filter_map(|(modifier, label)| { + // if modifier { + // Some( + // Label::new(label, key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // } else { + // None + // } + // }), + // ) + // .with_child( + // Label::new(keystroke.key.clone(), key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // .contained() + // .with_margin_left(keystroke_spacing) + // .flex_float() + // })) + // .contained() + // .with_style(style.container) + // .into_any() + // } +} + +fn humanize_action_name(name: &str) -> String { + let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); + let mut result = String::with_capacity(capacity); + for char in name.chars() { + if char == ':' { + if result.ends_with(':') { + result.push(' '); + } else { + result.push(':'); + } + } else if char == '_' { + result.push(' '); + } else if char.is_uppercase() { + if !result.ends_with(' ') { + result.push(' '); + } + result.extend(char.to_lowercase()); + } else { + result.push(char); + } + } + result +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command") + .field("name", &self.name) + .field("keystrokes", &self.keystrokes) + .finish() + } +} + +// #[cfg(test)] +// mod tests { +// use std::sync::Arc; + +// use super::*; +// use editor::Editor; +// use gpui::{executor::Deterministic, TestAppContext}; +// use project::Project; +// use workspace::{AppState, Workspace}; + +// #[test] +// fn test_humanize_action_name() { +// assert_eq!( +// humanize_action_name("editor::GoToDefinition"), +// "editor: go to definition" +// ); +// assert_eq!( +// humanize_action_name("editor::Backspace"), +// "editor: backspace" +// ); +// assert_eq!( +// humanize_action_name("go_to_line::Deploy"), +// "go to line: deploy" +// ); +// } + +// #[gpui::test] +// async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let project = Project::test(app_state.fs.clone(), [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let editor = window.add_view(cx, |cx| { +// let mut editor = Editor::single_line(None, cx); +// editor.set_text("abc", cx); +// editor +// }); + +// workspace.update(cx, |workspace, cx| { +// cx.focus(&editor); +// workspace.add_item(Box::new(editor.clone()), cx) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// // Fill up palette's command list by running an empty query; +// // we only need it to subsequently assert that the palette is initially +// // sorted by command's name. +// palette.delegate_mut().update_matches("".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// let is_sorted = +// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); +// assert!(is_sorted(&palette.delegate().actions)); +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, cx| { +// assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); +// palette.confirm(&Default::default(), cx); +// }); +// deterministic.run_until_parked(); +// editor.read_with(cx, |editor, cx| { +// assert_eq!(editor.text(cx), "ab"); +// }); + +// // Add namespace filter, and redeploy the palette +// cx.update(|cx| { +// cx.update_default_global::(|filter, _| { +// filter.filtered_namespaces.insert("editor"); +// }) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// // Assert editor command not present +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// assert!(palette.delegate().matches.is_empty()) +// }); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// editor::init(cx); +// workspace::init(app_state.clone(), cx); +// init(cx); +// Project::init_settings(cx); +// app_state +// }) +// } +// } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 8f0d94f3e8a25b79f48d1d5a2b81cc1b34e6d39c..9ec770e05cdb73fb3b3ddc172f9949d5217ddabf 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -8,25 +8,12 @@ use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{ModalEvent, Workspace}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace - .modal_layer() - .register_modal(Toggle, |workspace, cx| { - let editor = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::())?; - - Some(cx.build_view(|cx| GoToLine::new(editor, cx))) - }); - }, - ) - .detach(); + cx.observe_new_views(GoToLine::register).detach(); } pub struct GoToLine { @@ -38,14 +25,28 @@ pub struct GoToLine { } impl EventEmitter for GoToLine {} +impl Modal for GoToLine { + fn focus(&self, cx: &mut WindowContext) { + self.line_editor.update(cx, |editor, cx| editor.focus(cx)) + } +} impl GoToLine { - pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - let line_editor = cx.build_view(|cx| { - let editor = Editor::single_line(cx); - editor.focus(cx); - editor + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(editor) = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::()) + else { + return; + }; + + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); }); + } + + pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { + let line_editor = cx.build_view(|cx| Editor::single_line(cx)); let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); let editor = active_editor.read(cx); @@ -123,10 +124,6 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - self.active_editor.update(cx, |editor, cx| { - editor.focus(cx); - cx.notify(); - }); cx.emit(ModalEvent::Dismissed); } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 85149f5d55cc971844621e3acb3ba52d1d7c1a74..170ddf942f2bfcdacda710ef89094cd8aef726ec 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -4,7 +4,7 @@ use collections::{HashMap, HashSet}; use lazy_static::lazy_static; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; -use std::any::{type_name, Any}; +use std::any::{type_name, Any, TypeId}; /// Actions are used to implement keyboard-driven UI. /// When you declare an action, you can bind keys to the action in the keymap and @@ -100,6 +100,21 @@ where } } +impl dyn Action { + pub fn type_id(&self) -> TypeId { + self.as_any().type_id() + } + + pub fn name(&self) -> SharedString { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&self.type_id()) + .expect("type is not a registered action") + .clone() + } +} + type ActionBuilder = fn(json: Option) -> anyhow::Result>; lazy_static! { @@ -109,6 +124,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, + names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -117,9 +133,24 @@ pub fn register_action() { let name = A::qualified_name(); let mut lock = ACTION_REGISTRY.write(); lock.builders_by_name.insert(name.clone(), A::build); + lock.names_by_type_id + .insert(TypeId::of::(), name.clone()); lock.all_names.push(name); } +/// Construct an action based on its name and optional JSON parameters sourced from the keymap. +pub fn build_action_from_type(type_id: &TypeId) -> Result> { + let lock = ACTION_REGISTRY.read(); + let name = lock + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + drop(lock); + + build_action(&name, None) +} + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action(name: &str, params: Option) -> Result> { let lock = ACTION_REGISTRY.read(); diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index e1160227637c8374fa47e922a0fabb509308ec1e..2fe61f5909a0f6c2a07ab147c80962cee552ca17 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,6 +1,6 @@ use crate::{ - point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, - ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, + ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; @@ -9,6 +9,9 @@ use smallvec::SmallVec; use std::{cmp, ops::Range, sync::Arc}; use taffy::style::Overflow; +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visibile subset of items. pub fn uniform_list( id: Id, item_count: usize, @@ -20,9 +23,12 @@ where C: Component, { let id = id.into(); + let mut style = StyleRefinement::default(); + style.overflow.y = Some(Overflow::Hidden); + UniformList { id: id.clone(), - style: Default::default(), + style, item_count, render_items: Box::new(move |view, visible_range, cx| { f(view, visible_range, cx) @@ -86,8 +92,14 @@ impl Styled for UniformList { } } +#[derive(Default)] +pub struct UniformListState { + interactive: InteractiveElementState, + item_size: Size, +} + impl Element for UniformList { - type ElementState = InteractiveElementState; + type ElementState = UniformListState; fn id(&self) -> Option { Some(self.id.clone()) @@ -95,20 +107,47 @@ impl Element for UniformList { fn initialize( &mut self, - _: &mut V, + view_state: &mut V, element_state: Option, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { - element_state.unwrap_or_default() + element_state.unwrap_or_else(|| { + let item_size = self.measure_first_item(view_state, None, cx); + UniformListState { + interactive: InteractiveElementState::default(), + item_size, + } + }) } fn layout( &mut self, _view_state: &mut V, - _element_state: &mut Self::ElementState, + element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - cx.request_layout(&self.computed_style(), None) + let max_items = self.item_count; + let item_size = element_state.item_size; + let rem_size = cx.rem_size(); + + cx.request_measured_layout( + self.computed_style(), + rem_size, + move |known_dimensions: Size>, available_space: Size| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, + }; + size(width, height) + }, + ) } fn paint( @@ -133,12 +172,14 @@ impl Element for UniformList { cx.with_z_index(style.z_index.unwrap_or(0), |cx| { let content_size; if self.item_count > 0 { - let item_height = self.measure_item_height(view_state, padded_bounds, cx); + let item_height = self + .measure_first_item(view_state, Some(padded_bounds.size.width), cx) + .height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.lock().replace(ScrollHandleState { item_height, list_height: padded_bounds.size.height, - scroll_offset: element_state.track_scroll_offset(), + scroll_offset: element_state.interactive.track_scroll_offset(), }); } let visible_item_count = if item_height > px(0.) { @@ -147,6 +188,7 @@ impl Element for UniformList { 0 }; let scroll_offset = element_state + .interactive .scroll_offset() .map_or((0.0).into(), |offset| offset.y); let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; @@ -190,20 +232,25 @@ impl Element for UniformList { let overflow = point(style.overflow.x, Overflow::Scroll); cx.with_z_index(0, |cx| { - self.interactivity - .paint(bounds, content_size, overflow, element_state, cx); + self.interactivity.paint( + bounds, + content_size, + overflow, + &mut element_state.interactive, + cx, + ); }); }) } } impl UniformList { - fn measure_item_height( + fn measure_first_item( &self, view_state: &mut V, - list_bounds: Bounds, + list_width: Option, cx: &mut ViewContext, - ) -> Pixels { + ) -> Size { let mut items = (self.render_items)(view_state, 0..1, cx); debug_assert!(items.len() == 1); let mut item_to_measure = items.pop().unwrap(); @@ -212,11 +259,13 @@ impl UniformList { cx.compute_layout( layout_id, Size { - width: AvailableSpace::Definite(list_bounds.size.width), + width: list_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), height: AvailableSpace::MinContent, }, ); - cx.layout_bounds(layout_id).size.height + cx.layout_bounds(layout_id).size } pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b9cbb073f3539133c29b1714fffd951..243eb3cb07844a2fa558d6c1bf2555f75cf1af95 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -94,7 +94,6 @@ pub trait StatelessInteractive: Element { fn on_mouse_down_out( mut self, - button: MouseButton, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, ) -> Self where @@ -103,10 +102,7 @@ pub trait StatelessInteractive: Element { self.stateless_interactivity() .mouse_down_listeners .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.contains_point(&event.position) - { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { handler(view, event, cx) } })); diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b424f83c5785d7dd0b33c776054bbb3..00e1e55cd57d0fc96deb8d57be7f68f3c3b1f5bd 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -184,6 +184,10 @@ impl AnyView { .compute_layout(layout_id, available_space); (self.paint)(self, &mut rendered_element, cx); } + + pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) { + (self.initialize)(self, cx); + } } impl Component for AnyView { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index ac7dcf02569e4db6c3c8f92658d95088dcdc4ba5..b020366ad02659a158128f73f3497f5d994fad8c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,14 +1,15 @@ use crate::{ - px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId, - Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, - GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, - KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, - RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, - SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, - UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, + DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, + FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, + IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, + MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, + SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, + TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -145,6 +146,11 @@ impl FocusHandle { } } + /// Moves the focus to the element associated with this handle. + pub fn focus(&self, cx: &mut WindowContext) { + cx.focus(self) + } + /// Obtains whether the element associated with this handle is currently focused. pub fn is_focused(&self, cx: &WindowContext) -> bool { self.id.is_focused(cx) @@ -227,7 +233,7 @@ pub(crate) struct Frame { key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, - key_dispatch_stack: Vec, + pub(crate) key_dispatch_stack: Vec, freeze_key_dispatch_stack: bool, focus_parents_by_child: HashMap, pub(crate) scene_builder: SceneBuilder, @@ -326,7 +332,7 @@ impl Window { /// find the focused element. We interleave key listeners with dispatch contexts so we can use the /// contexts when matching key events against the keymap. A key listener can be either an action /// handler or a [KeyDown] / [KeyUp] event listener. -enum KeyDispatchStackFrame { +pub(crate) enum KeyDispatchStackFrame { Listener { event_type: TypeId, listener: AnyKeyListener, @@ -401,11 +407,18 @@ impl<'a> WindowContext<'a> { /// Move focus to the element associated with the given `FocusHandle`. pub fn focus(&mut self, handle: &FocusHandle) { + if self.window.focus == Some(handle.id) { + return; + } + if self.window.last_blur.is_none() { self.window.last_blur = Some(self.window.focus); } self.window.focus = Some(handle.id); + + // self.window.current_frame.key_dispatch_stack.clear() + // self.window.root_view.initialize() self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(handle.id), @@ -427,6 +440,14 @@ impl<'a> WindowContext<'a> { self.notify(); } + pub fn dispatch_action(&mut self, action: Box) { + self.defer(|cx| { + cx.app.propagate_event = true; + let stack = cx.dispatch_stack(); + cx.dispatch_action_internal(action, &stack[..]) + }) + } + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { @@ -1054,6 +1075,26 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } + pub(crate) fn dispatch_stack(&mut self) -> Vec { + let root_view = self.window.root_view.take().unwrap(); + let window = &mut *self.window; + let mut spare_frame = Frame::default(); + mem::swap(&mut spare_frame, &mut window.previous_frame); + + self.start_frame(); + + root_view.draw_dispatch_stack(self); + + let window = &mut *self.window; + // restore the old values of current and previous frame, + // putting the new frame into spare_frame. + mem::swap(&mut window.current_frame, &mut window.previous_frame); + mem::swap(&mut spare_frame, &mut window.previous_frame); + self.window.root_view = Some(root_view); + + spare_frame.key_dispatch_stack + } + /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { @@ -1196,7 +1237,7 @@ impl<'a> WindowContext<'a> { DispatchPhase::Capture, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal(action, &key_dispatch_stack[..ix]); } if !self.app.propagate_event { break; @@ -1223,7 +1264,10 @@ impl<'a> WindowContext<'a> { DispatchPhase::Bubble, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal( + action, + &key_dispatch_stack[..ix], + ); } if !self.app.propagate_event { @@ -1295,7 +1339,29 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - fn dispatch_action( + pub fn available_actions(&self) -> impl Iterator> + '_ { + let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack; + key_dispatch_stack.iter().filter_map(|frame| { + match frame { + // todo!factor out a KeyDispatchStackFrame::Action + KeyDispatchStackFrame::Listener { + event_type, + listener: _, + } => { + match build_action_from_type(event_type) { + Ok(action) => Some(action), + Err(err) => { + dbg!(err); + None + } // we'll hit his if TypeId == KeyDown + } + } + KeyDispatchStackFrame::Context(_) => None, + } + }) + } + + pub(crate) fn dispatch_action_internal( &mut self, action: Box, dispatch_stack: &[KeyDispatchStackFrame], diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 90e1ae931c916f7f04ab60915026902342894a20..3c4d21ad50b0a6c7bd7959a615cf9336adf458f9 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { package = "editor2", path = "../editor2" } +ui = { package = "ui2", path = "../ui2" } gpui = { package = "gpui2", path = "../gpui2" } menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 075cf10ff6811eeb2ce8f8a82f31f9799c0a4a00..9d0019b2dc92a156659aca494987c4160f438e12 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -5,6 +5,8 @@ use gpui::{ WindowContext, }; use std::cmp; +use theme::ActiveTheme; +use ui::v_stack; pub struct Picker { pub delegate: D, @@ -57,6 +59,7 @@ impl Picker { let ix = cmp::min(index + 1, count - 1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); + cx.notify(); } } @@ -67,6 +70,7 @@ impl Picker { let ix = index.saturating_sub(1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); + cx.notify(); } } @@ -75,6 +79,7 @@ impl Picker { if count > 0 { self.delegate.set_selected_index(0, cx); self.scroll_handle.scroll_to_item(0); + cx.notify(); } } @@ -83,6 +88,7 @@ impl Picker { if count > 0 { self.delegate.set_selected_index(count - 1, cx); self.scroll_handle.scroll_to_item(count - 1); + cx.notify(); } } @@ -133,7 +139,7 @@ impl Picker { impl Render for Picker { type Element = Div, FocusEnabled>; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") .id("picker-container") @@ -146,18 +152,38 @@ impl Render for Picker { .on_action(Self::cancel) .on_action(Self::confirm) .on_action(Self::secondary_confirm) - .child(self.editor.clone()) .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()) - .size_full(), + v_stack().gap_px().child( + v_stack() + .py_0p5() + .px_1() + .child(div().px_2().py_0p5().child(self.editor.clone())), + ), + ) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) + .child( + v_stack() + .py_0p5() + .px_1() + .grow() + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()), + ) + .max_h_72() + .overflow_hidden(), ) } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a5760380f5efa6f138f237a0eb3ae3a0e34216de..09ffa6c13f92b892ad1717f200a8c1ced84c0361 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,15 +1,22 @@ -use crate::Workspace; use gpui::{ - div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatelessInteractive, - Styled, Subscription, View, ViewContext, + div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive, + Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; -use std::{any::TypeId, sync::Arc}; use ui::v_stack; +pub struct ActiveModal { + modal: AnyView, + subscription: Subscription, + previous_focus_handle: Option, + focus_handle: FocusHandle, +} + pub struct ModalLayer { - open_modal: Option, - subscription: Option, - registered_modals: Vec<(TypeId, Box) -> Div>)>, + active_modal: Option, +} + +pub trait Modal: Render + EventEmitter { + fn focus(&self, cx: &mut WindowContext); } pub enum ModalEvent { @@ -18,98 +25,82 @@ pub enum ModalEvent { impl ModalLayer { pub fn new() -> Self { - Self { - open_modal: None, - subscription: None, - registered_modals: Vec::new(), - } + Self { active_modal: None } } - pub fn register_modal(&mut self, action: A, build_view: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: EventEmitter + Render, - B: Fn(&mut Workspace, &mut ViewContext) -> Option> + 'static, + V: Modal, + B: FnOnce(&mut ViewContext) -> V, { - let build_view = Arc::new(build_view); - - self.registered_modals.push(( - TypeId::of::(), - Box::new(move |mut div| { - let build_view = build_view.clone(); - - div.on_action(move |workspace, event: &A, cx| { - let Some(new_modal) = (build_view)(workspace, cx) else { - return; - }; - workspace.modal_layer().show_modal(new_modal, cx); - }) - }), - )); + let previous_focus = cx.focused(); + + if let Some(active_modal) = &self.active_modal { + let is_close = active_modal.modal.clone().downcast::().is_ok(); + self.hide_modal(cx); + if is_close { + return; + } + } + let new_modal = cx.build_view(build_view); + self.show_modal(new_modal, cx); } - pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) + pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) where - V: EventEmitter + Render, + V: Modal, { - self.subscription = Some(cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.modal_layer().hide_modal(cx), - })); - self.open_modal = Some(new_modal.into()); - cx.notify(); - } - - pub fn hide_modal(&mut self, cx: &mut ViewContext) { - self.open_modal.take(); - self.subscription.take(); + self.active_modal = Some(ActiveModal { + modal: new_modal.clone().into(), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { + ModalEvent::Dismissed => this.hide_modal(cx), + }), + previous_focus_handle: cx.focused(), + focus_handle: cx.focus_handle(), + }); + new_modal.update(cx, |modal, cx| modal.focus(cx)); cx.notify(); } - pub fn wrapper_element(&self, cx: &ViewContext) -> Div { - let mut parent = div().relative().size_full(); - - for (_, action) in self.registered_modals.iter() { - parent = (action)(parent); + pub fn hide_modal(&mut self, cx: &mut ViewContext) { + if let Some(active_modal) = self.active_modal.take() { + if let Some(previous_focus) = active_modal.previous_focus_handle { + if active_modal.focus_handle.contains_focused(cx) { + previous_focus.focus(cx); + } + } } - parent.when_some(self.open_modal.as_ref(), |parent, open_modal| { - let container1 = div() - .absolute() - .flex() - .flex_col() - .items_center() - .size_full() - .top_0() - .left_0() - .z_index(400); - - // transparent layer - let container2 = v_stack().h(px(0.0)).relative().top_20(); - - parent.child(container1.child(container2.child(open_modal.clone()))) - }) + cx.notify(); } } -// impl Render for ModalLayer { -// type Element = Div; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// let mut div = div(); -// for (type_id, build_view) in cx.global::().registered_modals { -// div = div.useful_on_action( -// type_id, -// Box::new(|this, _: dyn Any, phase, cx: &mut ViewContext| { -// if phase == DispatchPhase::Capture { -// return; -// } -// self.workspace.update(cx, |workspace, cx| { -// self.open_modal = Some(build_view(workspace, cx)); -// }); -// cx.notify(); -// }), -// ) -// } - -// div -// } -// } +impl Render for ModalLayer { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let Some(active_modal) = &self.active_modal else { + return div(); + }; + + div() + .absolute() + .flex() + .flex_col() + .items_center() + .size_full() + .top_0() + .left_0() + .z_index(400) + .child( + v_stack() + .h(px(0.0)) + .top_20() + .track_focus(&active_modal.focus_handle) + .on_mouse_down_out(|this: &mut Self, event, cx| { + this.hide_modal(cx); + }) + .child(active_modal.modal.clone()), + ) + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 24ec810ac5be090cb4522d8f2d699c89b3928d1b..5c678df317ac9a97e858f8e0ba3be8e9fdb89b6c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,11 +36,12 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, - GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, + AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, + EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, + Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, + Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -530,6 +531,13 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, focus_handle: FocusHandle, + workspace_actions: Vec< + Box< + dyn Fn( + Div>, + ) -> Div>, + >, + >, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -542,7 +550,7 @@ pub struct Workspace { last_active_center_pane: Option>, last_active_view_id: Option, status_bar: View, - modal_layer: ModalLayer, + modal_layer: View, // titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, @@ -694,7 +702,7 @@ impl Workspace { }); let workspace_handle = cx.view().downgrade(); - let modal_layer = ModalLayer::new(); + let modal_layer = cx.build_view(|cx| ModalLayer::new()); // todo!() // cx.update_default_global::, _, _>(|drag_and_drop, _| { @@ -775,13 +783,10 @@ impl Workspace { leader_updates_tx, subscriptions, pane_history_timestamp, + workspace_actions: Default::default(), } } - pub fn modal_layer(&mut self) -> &mut ModalLayer { - &mut self.modal_layer - } - fn new_local( abs_paths: Vec, app_state: Arc, @@ -3495,6 +3500,35 @@ impl Workspace { // ) // } // } + pub fn register_action( + &mut self, + callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, + ) { + let callback = Arc::new(callback); + + self.workspace_actions.push(Box::new(move |div| { + let callback = callback.clone(); + div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx)) + })); + } + + fn add_workspace_actions_listeners( + &self, + mut div: Div>, + ) -> Div> { + for action in self.workspace_actions.iter() { + div = (action)(div) + } + div + } + + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + where + B: FnOnce(&mut ViewContext) -> V, + { + self.modal_layer + .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) + } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { @@ -3709,157 +3743,160 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans") - .gap_0() - .justify_start() - .items_start() - .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().background) - .child(self.render_titlebar(cx)) - .child( - // todo! should this be a component a view? - self.modal_layer - .wrapper_element(cx) - .relative() - .flex_1() - .w_full() - .flex() - .overflow_hidden() - .border_t() - .border_b() - .border_color(cx.theme().colors().border) - // .children( - // Some( - // Panel::new("project-panel-outer", cx) - // .side(PanelSide::Left) - // .child(ProjectPanel::new("project-panel-inner")), - // ) - // .filter(|_| self.is_project_panel_open()), - // ) - // .children( - // Some( - // Panel::new("collab-panel-outer", cx) - // .child(CollabPanel::new("collab-panel-inner")) - // .side(PanelSide::Left), - // ) - // .filter(|_| self.is_collab_panel_open()), - // ) - // .child(NotificationToast::new( - // "maxbrunsfeld has requested to add you as a contact.".into(), - // )) - .child( - div().flex().flex_col().flex_1().h_full().child( - div().flex().flex_1().child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )), + let mut context = DispatchContext::default(); + context.insert("Workspace"); + cx.with_key_dispatch_context(context, |cx| { + div() + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + // todo! should this be a component a view? + self.add_workspace_actions_listeners(div().id("workspace")) + .relative() + .flex_1() + .w_full() + .flex() + .overflow_hidden() + .border_t() + .border_b() + .border_color(cx.theme().colors().border) + .child(self.modal_layer.clone()) + // .children( + // Some( + // Panel::new("project-panel-outer", cx) + // .side(PanelSide::Left) + // .child(ProjectPanel::new("project-panel-inner")), + // ) + // .filter(|_| self.is_project_panel_open()), + // ) + // .children( + // Some( + // Panel::new("collab-panel-outer", cx) + // .child(CollabPanel::new("collab-panel-inner")) + // .side(PanelSide::Left), + // ) + // .filter(|_| self.is_collab_panel_open()), + // ) + // .child(NotificationToast::new( + // "maxbrunsfeld has requested to add you as a contact.".into(), + // )) + .child( + div().flex().flex_col().flex_1().h_full().child( + div().flex().flex_1().child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )), + ), // .children( + // Some( + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), + // ) + // .filter(|_| self.is_terminal_open()), + // ), ), // .children( // Some( - // Panel::new("terminal-panel", cx) - // .child(Terminal::new()) - // .allowed_sides(PanelAllowedSides::BottomOnly) - // .side(PanelSide::Bottom), + // Panel::new("chat-panel-outer", cx) + // .side(PanelSide::Right) + // .child(ChatPanel::new("chat-panel-inner").messages(vec![ + // ChatMessage::new( + // "osiewicz".to_string(), + // "is this thing on?".to_string(), + // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ChatMessage::new( + // "maxdeviant".to_string(), + // "Reading you loud and clear!".to_string(), + // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + // .unwrap() + // .naive_local(), + // ), + // ])), + // ) + // .filter(|_| self.is_chat_panel_open()), + // ) + // .children( + // Some( + // Panel::new("notifications-panel-outer", cx) + // .side(PanelSide::Right) + // .child(NotificationsPanel::new("notifications-panel-inner")), + // ) + // .filter(|_| self.is_notifications_panel_open()), + // ) + // .children( + // Some( + // Panel::new("assistant-panel-outer", cx) + // .child(AssistantPanel::new("assistant-panel-inner")), // ) - // .filter(|_| self.is_terminal_open()), + // .filter(|_| self.is_assistant_panel_open()), // ), - ), // .children( - // Some( - // Panel::new("chat-panel-outer", cx) - // .side(PanelSide::Right) - // .child(ChatPanel::new("chat-panel-inner").messages(vec![ - // ChatMessage::new( - // "osiewicz".to_string(), - // "is this thing on?".to_string(), - // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ChatMessage::new( - // "maxdeviant".to_string(), - // "Reading you loud and clear!".to_string(), - // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ])), - // ) - // .filter(|_| self.is_chat_panel_open()), - // ) - // .children( - // Some( - // Panel::new("notifications-panel-outer", cx) - // .side(PanelSide::Right) - // .child(NotificationsPanel::new("notifications-panel-inner")), - // ) - // .filter(|_| self.is_notifications_panel_open()), - // ) - // .children( - // Some( - // Panel::new("assistant-panel-outer", cx) - // .child(AssistantPanel::new("assistant-panel-inner")), - // ) - // .filter(|_| self.is_assistant_panel_open()), - // ), - ) - .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) - .z_index(8) - // Debug - .child( - div() - .flex() - .flex_col() - .z_index(9) - .absolute() - .top_20() - .left_1_4() - .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), - ) + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) + }) } } - // todo!() // impl Entity for Workspace { // type Event = Event; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 661ab0c2934b4b3d34ed1547d3861e64ece37737..570912abc579a7aad2461c4fe081354c97fcc478 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -25,7 +25,7 @@ call = { package = "call2", path = "../call2" } cli = { path = "../cli" } # collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } -# command_palette = { path = "../command_palette" } +command_palette = { package="command_palette2", path = "../command_palette2" } # component_test = { path = "../component_test" } # context_menu = { path = "../context_menu" } client = { package = "client2", path = "../client2" } @@ -74,7 +74,7 @@ util = { path = "../util" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } # welcome = { path = "../welcome" } -# zed-actions = {path = "../zed-actions"} +zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index cd0f8e5fbf7333d7849de040e7dee211f6738549..c9e7ee8c580eb4f4b694855376d3b632c4fb22a8 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -142,7 +142,7 @@ fn main() { // context_menu::init(cx); project::Project::init(&client, cx); client::init(&client, cx); - // command_palette::init(cx); + command_palette::init(cx); language::init(cx); editor::init(cx); copilot::init( @@ -761,7 +761,7 @@ fn load_embedded_fonts(cx: &AppContext) { // #[cfg(not(debug_assertions))] // async fn watch_languages(_: Arc, _: Arc) -> Option<()> { // None -// } +// // #[cfg(not(debug_assertions))] // fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} diff --git a/crates/zed_actions2/Cargo.toml b/crates/zed_actions2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b3b5b4ce578dbe69ffe0e7998c1089d5e4e6ac3a --- /dev/null +++ b/crates/zed_actions2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "zed_actions2" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gpui = { package = "gpui2", path = "../gpui2" } +serde.workspace = true diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..090352b2ccada82e4e5675384ac5b8a9bad8c90b --- /dev/null +++ b/crates/zed_actions2/src/lib.rs @@ -0,0 +1,34 @@ +use gpui::{action, actions}; + +actions!( + About, + DebugElements, + DecreaseBufferFontSize, + Hide, + HideOthers, + IncreaseBufferFontSize, + Minimize, + OpenDefaultKeymap, + OpenDefaultSettings, + OpenKeymap, + OpenLicenses, + OpenLocalSettings, + OpenLog, + OpenSettings, + OpenTelemetryLog, + Quit, + ResetBufferFontSize, + ResetDatabase, + ShowAll, + ToggleFullScreen, + Zoom, +); + +#[action] +pub struct OpenBrowser { + pub url: String, +} +#[action] +pub struct OpenZedURL { + pub url: String, +}