From d184e0d4269fd8932ccff0ae2a77f315d8986ab0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:54:05 +0100 Subject: [PATCH 01/10] Start working on command_palette2 --- Cargo.lock | 34 ++ Cargo.toml | 4 +- assets/keymaps/default.json | 3 +- crates/command_palette2/Cargo.toml | 34 ++ .../command_palette2/src/command_palette.rs | 542 ++++++++++++++++++ crates/gpui2/src/action.rs | 7 +- crates/workspace2/src/workspace2.rs | 301 +++++----- crates/zed2/Cargo.toml | 4 +- crates/zed2/src/main.rs | 4 +- crates/zed_actions2/Cargo.toml | 11 + crates/zed_actions2/src/lib.rs | 34 ++ 11 files changed, 822 insertions(+), 156 deletions(-) create mode 100644 crates/command_palette2/Cargo.toml create mode 100644 crates/command_palette2/src/command_palette.rs create mode 100644 crates/zed_actions2/Cargo.toml create mode 100644 crates/zed_actions2/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ded64052c8..4143cf8fa7 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" @@ -11362,6 +11386,7 @@ dependencies = [ "cli", "client2", "collections", + "command_palette2", "copilot2", "ctor", "db2", @@ -11448,6 +11473,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 1b8081d066..905750f835 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/assets/keymaps/default.json b/assets/keymaps/default.json index ef6a655bdc..b18cb4a7ae 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -387,7 +387,8 @@ } }, { - "context": "Workspace", + // todo!() fix context + // "context": "Workspace", "bindings": { "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml new file mode 100644 index 0000000000..bcc0099c20 --- /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 0000000000..dd0490082e --- /dev/null +++ b/crates/command_palette2/src/command_palette.rs @@ -0,0 +1,542 @@ +use anyhow::anyhow; +use collections::{CommandPaletteFilter, HashMap}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element, + EventEmitter, FocusHandle, Keystroke, ParentElement, Render, View, ViewContext, VisualContext, + WeakView, +}; +use picker::{Picker, PickerDelegate}; +use std::cmp::{self, Reverse}; +use ui::modal; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, + ResultExt, +}; +use workspace::{ModalEvent, Workspace}; +use zed_actions::OpenZedURL; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + dbg!("init"); + cx.set_global(HitCounts::default()); + + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + dbg!("new workspace found"); + workspace + .modal_layer() + .register_modal(Toggle, |workspace, cx| { + dbg!("hitting cmd-shift-p"); + let Some(focus_handle) = cx.focused() else { + return None; + }; + + Some(cx.build_view(|cx| { + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle); + CommandPalette::new(delegate, cx) + })) + }); + }, + ) + .detach(); +} + +pub struct CommandPalette { + picker: View>, +} + +impl CommandPalette { + fn new(delegate: CommandPaletteDelegate, cx: &mut ViewContext) -> Self { + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} +impl EventEmitter for CommandPalette {} + +impl Render for CommandPalette { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + dbg!("Rendering"); + 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, + actions: Vec, + matches: Vec, + selected_ix: usize, + focus_handle: FocusHandle, +} + +pub enum Event { + Dismissed, + Confirmed { + window: AnyWindowHandle, + focused_view_id: usize, + action: Box, + }, +} + +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 { + pub fn new(command_palette: WeakView, focus_handle: FocusHandle) -> Self { + Self { + command_palette, + actions: Default::default(), + matches: vec![StringMatch { + candidate_id: 0, + score: 0., + positions: vec![], + string: "Foo my bar".into(), + }], + selected_ix: 0, + 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 view_id = &self.focus_handle; + let window = cx.window(); + cx.spawn(move |picker, mut cx| async move { + let mut actions = picker + .update(&mut cx, |this, _| this.delegate.actions.clone()) + .expect("todo: handle picker no longer being around"); + // _ = window + // .available_actions(view_id, &cx) + // .into_iter() + // .flatten() + // .filter_map(|(name, action, bindings)| { + // let filtered = cx.read(|cx| { + // if cx.has_global::() { + // let filter = cx.global::(); + // filter.filtered_namespaces.contains(action.namespace()) + // } else { + // false + // } + // }); + + // if filtered { + // None + // } else { + // Some(Command { + // name: humanize_action_name(name), + // action, + // keystrokes: bindings + // .iter() + // .map(|binding| binding.keystrokes()) + // .last() + // .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), + // }) + // } + // }) + // .collect::>(); + + cx.read_global::(|hit_counts, _| { + actions.sort_by_key(|action| { + ( + Reverse(hit_counts.0.get(&action.name).cloned()), + action.name.clone(), + ) + }); + }) + .ok(); + + let candidates = actions + .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 = None; + // todo!() for vim mode + // cx.read(|cx| { + // if cx.has_global::() { + // cx.global::()(&query, cx) + // } else { + // None + // } + // }); + 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| actions[m.candidate_id].action.type_id() == action.type_id()) + { + matches.remove(idx); + } + actions.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: actions.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } + picker + .update(&mut cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.actions = actions; + 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>) { + dbg!("dismissed"); + self.command_palette + .update(cx, |command_palette, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + // if !self.matches.is_empty() { + // let window = cx.window(); + // let focused_view_id = self.focused_view_id; + // let action_ix = self.matches[self.selected_ix].candidate_id; + // let command = self.actions.remove(action_ix); + // cx.update_default_global(|hit_counts: &mut HitCounts, _| { + // *hit_counts.0.entry(command.name).or_default() += 1; + // }); + // let action = command.action; + + // cx.app_context() + // .spawn(move |mut cx| async move { + // window + // .dispatch_action(focused_view_id, action.as_ref(), &mut cx) + // .ok_or_else(|| anyhow!("window was closed")) + // }) + // .detach_and_log_err(cx); + // } + self.dismissed(cx) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem { + div().child("ooh yeah") + } + + // 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/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 85149f5d55..5cd5eb4cdb 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,11 @@ where } } +impl dyn Action { + pub fn type_id(&self) -> TypeId { + self.as_any().type_id() + } +} type ActionBuilder = fn(json: Option) -> anyhow::Result>; lazy_static! { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 24ec810ac5..c91c388f2a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,10 +37,10 @@ use futures::{ }; 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, + AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, EventEmitter, + FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3709,157 +3709,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.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, + )), + ), // .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_terminal_open()), + // .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()), // ), - ), // .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 661ab0c293..570912abc5 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 cd0f8e5fbf..c9e7ee8c58 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 0000000000..b3b5b4ce57 --- /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 0000000000..090352b2cc --- /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, +} From 194d6156916671f035b454118bfdae0eccb65791 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:33:36 +0100 Subject: [PATCH 02/10] Fix up keybindings propagation Co-authored-by: Conrad --- assets/keymaps/default.json | 3 +-- .../command_palette2/src/command_palette.rs | 5 ++--- crates/editor2/src/element.rs | 4 ++++ crates/gpui2/src/interactive.rs | 1 + crates/gpui2/src/keymap/binding.rs | 13 ++++++++++++ crates/gpui2/src/keymap/matcher.rs | 1 + crates/workspace2/src/modal_layer.rs | 20 ++++++++++++++----- crates/workspace2/src/workspace2.rs | 7 ++++--- 8 files changed, 41 insertions(+), 13 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index b18cb4a7ae..ef6a655bdc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -387,8 +387,7 @@ } }, { - // todo!() fix context - // "context": "Workspace", + "context": "Workspace", "bindings": { "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index dd0490082e..46b099ea3c 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -3,8 +3,8 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element, - EventEmitter, FocusHandle, Keystroke, ParentElement, Render, View, ViewContext, VisualContext, - WeakView, + EventEmitter, FocusHandle, Keystroke, ParentElement, Render, Styled, View, ViewContext, + VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; @@ -60,7 +60,6 @@ impl Render for CommandPalette { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - dbg!("Rendering"); modal(cx).w_96().child(self.picker.clone()) } } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3e77a66936..8dbe989b1f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -4149,12 +4149,16 @@ fn build_key_listeners( build_key_listener( move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { if phase == DispatchPhase::Bubble { + dbg!(&dispatch_context); if let KeyMatch::Some(action) = cx.match_keystroke( &global_element_id, &key_down.keystroke, dispatch_context, ) { + dbg!("got action", &action); return Some(action); + } else { + dbg!("not action"); } } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b..51efde62c1 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -414,6 +414,7 @@ pub trait ElementInteractivity: 'static { Box::new(move |_, key_down, context, phase, cx| { if phase == DispatchPhase::Bubble { let key_down = key_down.downcast_ref::().unwrap(); + dbg!(&context); if let KeyMatch::Some(action) = cx.match_keystroke(&global_id, &key_down.keystroke, context) { diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 829f7a3b2c..67041dc488 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -44,6 +44,19 @@ impl KeyBinding { pending_keystrokes: &[Keystroke], contexts: &[&DispatchContext], ) -> KeyMatch { + let should_debug = self.keystrokes.len() == 1 + && self.keystrokes[0].key == "p" + && self.keystrokes[0].modifiers.command == true + && self.keystrokes[0].modifiers.shift == true; + + if false && should_debug { + dbg!( + &self.keystrokes, + &pending_keystrokes, + &contexts, + &self.matches_context(contexts) + ); + } if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) { diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c2033a9595..c86b65c47e 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -46,6 +46,7 @@ impl KeyMatcher { keystroke: &Keystroke, context_stack: &[&DispatchContext], ) -> KeyMatch { + dbg!(keystroke, &context_stack); let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. if keymap.version() != self.keymap_version { diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a5760380f5..fc85ae8351 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,7 +1,7 @@ use crate::Workspace; use gpui::{ - div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatelessInteractive, - Styled, Subscription, View, ViewContext, + div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatefulInteractivity, + StatelessInteractive, Styled, Subscription, View, ViewContext, }; use std::{any::TypeId, sync::Arc}; use ui::v_stack; @@ -9,7 +9,14 @@ use ui::v_stack; pub struct ModalLayer { open_modal: Option, subscription: Option, - registered_modals: Vec<(TypeId, Box) -> Div>)>, + registered_modals: Vec<( + TypeId, + Box< + dyn Fn( + Div>, + ) -> Div>, + >, + )>, } pub enum ModalEvent { @@ -64,8 +71,11 @@ impl ModalLayer { cx.notify(); } - pub fn wrapper_element(&self, cx: &ViewContext) -> Div { - let mut parent = div().relative().size_full(); + pub fn wrapper_element( + &self, + cx: &ViewContext, + ) -> Div> { + let mut parent = div().id("modal layer").relative().size_full(); for (_, action) in self.registered_modals.iter() { parent = (action)(parent); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c91c388f2a..54c8709d7e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -39,8 +39,8 @@ use gpui::{ actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + StatefulInteractive, StatefulInteractivity, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3706,13 +3706,14 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div; + type Element = Div>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = DispatchContext::default(); context.insert("Workspace"); cx.with_key_dispatch_context(context, |cx| { div() + .id("workspace") .relative() .size_full() .flex() From a1d9f351dbb33cb2a38884df8095aa3c33260226 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:51:37 +0100 Subject: [PATCH 03/10] Some more woogaloo around action dispatch Co-authored-by: Conrad --- .../command_palette2/src/command_palette.rs | 3 ++ crates/gpui2/src/action.rs | 14 ++++++ crates/gpui2/src/window.rs | 47 +++++++++++++++---- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 46b099ea3c..508891be9e 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -33,6 +33,9 @@ pub fn init(cx: &mut AppContext) { return None; }; + let available_actions = cx.available_actions(); + dbg!(&available_actions); + Some(cx.build_view(|cx| { let delegate = CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle); diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 5cd5eb4cdb..3a1832e58c 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -114,6 +114,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, + builders_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -122,9 +123,22 @@ 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.builders_by_type_id.insert(TypeId::of::(), A::build); 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 build_action = lock + .builders_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?; + + (build_action)(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/window.rs b/crates/gpui2/src/window.rs index ac7dcf0256..123a516b02 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; @@ -1295,6 +1296,32 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } + pub fn available_actions(&mut self) -> Vec> { + let key_dispatch_stack = &self.window.current_frame.key_dispatch_stack; + let mut actions = Vec::new(); + dbg!(key_dispatch_stack.len()); + for frame in key_dispatch_stack { + match frame { + // todo!factor out a KeyDispatchStackFrame::Action + KeyDispatchStackFrame::Listener { + event_type, + listener: _, + } => { + match build_action_from_type(event_type) { + Ok(action) => { + actions.push(action); + } + Err(err) => { + dbg!(err); + } // we'll hit his if TypeId == KeyDown + } + } + KeyDispatchStackFrame::Context(_) => {} + } + } + actions + } + fn dispatch_action( &mut self, action: Box, From fa153a0d56d79fab18c9e8d341c688312cce90c4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 13:23:30 -0700 Subject: [PATCH 04/10] Make command dispatching work --- Cargo.lock | 1 + .../command_palette2/src/command_palette.rs | 150 ++++++++++-------- crates/editor2/src/element.rs | 4 - crates/gpui2/src/action.rs | 26 ++- crates/gpui2/src/interactive.rs | 1 - crates/gpui2/src/keymap/binding.rs | 13 -- crates/gpui2/src/keymap/matcher.rs | 1 - crates/gpui2/src/view.rs | 6 +- crates/gpui2/src/window.rs | 62 ++++++-- crates/picker2/Cargo.toml | 1 + crates/picker2/src/picker2.rs | 41 +++-- 11 files changed, 188 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4143cf8fa7..36c1a62d7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6153,6 +6153,7 @@ dependencies = [ "serde_json", "settings2", "theme2", + "ui2", "util", ] diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 508891be9e..6816ecf3a2 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,13 +2,14 @@ use anyhow::anyhow; use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Div, Element, - EventEmitter, FocusHandle, Keystroke, ParentElement, Render, Styled, View, ViewContext, - VisualContext, WeakView, + actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Component, Div, + Element, EventEmitter, FocusHandle, Keystroke, ParentElement, Render, StatelessInteractive, + Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; -use ui::modal; +use theme::ActiveTheme; +use ui::{modal, Label}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -19,29 +20,17 @@ use zed_actions::OpenZedURL; actions!(Toggle); pub fn init(cx: &mut AppContext) { - dbg!("init"); cx.set_global(HitCounts::default()); cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { - dbg!("new workspace found"); - workspace - .modal_layer() - .register_modal(Toggle, |workspace, cx| { - dbg!("hitting cmd-shift-p"); - let Some(focus_handle) = cx.focused() else { - return None; - }; + workspace.modal_layer().register_modal(Toggle, |_, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return None; + }; - let available_actions = cx.available_actions(); - dbg!(&available_actions); - - Some(cx.build_view(|cx| { - let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), focus_handle); - CommandPalette::new(delegate, cx) - })) - }); + Some(cx.build_view(|cx| CommandPalette::new(previous_focus_handle, cx))) + }); }, ) .detach(); @@ -52,8 +41,35 @@ pub struct CommandPalette { } impl CommandPalette { - fn new(delegate: CommandPaletteDelegate, cx: &mut ViewContext) -> Self { - let picker = cx.build_view(|cx| Picker::new(delegate, 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, cx); + + let picker = cx.build_view(|cx| { + let picker = Picker::new(delegate, cx); + picker.focus(cx); + picker + }); Self { picker } } } @@ -78,19 +94,10 @@ pub struct CommandInterceptResult { pub struct CommandPaletteDelegate { command_palette: WeakView, - actions: Vec, + commands: Vec, matches: Vec, selected_ix: usize, - focus_handle: FocusHandle, -} - -pub enum Event { - Dismissed, - Confirmed { - window: AnyWindowHandle, - focused_view_id: usize, - action: Box, - }, + previous_focus_handle: FocusHandle, } struct Command { @@ -115,10 +122,15 @@ impl Clone for Command { struct HitCounts(HashMap); impl CommandPaletteDelegate { - pub fn new(command_palette: WeakView, focus_handle: FocusHandle) -> Self { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + cx: &ViewContext, + ) -> Self { Self { command_palette, - actions: Default::default(), + commands, matches: vec![StringMatch { candidate_id: 0, score: 0., @@ -126,7 +138,7 @@ impl CommandPaletteDelegate { string: "Foo my bar".into(), }], selected_ix: 0, - focus_handle, + previous_focus_handle, } } } @@ -151,11 +163,11 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let view_id = &self.focus_handle; + let view_id = &self.previous_focus_handle; let window = cx.window(); cx.spawn(move |picker, mut cx| async move { let mut actions = picker - .update(&mut cx, |this, _| this.delegate.actions.clone()) + .update(&mut cx, |this, _| this.delegate.commands.clone()) .expect("todo: handle picker no longer being around"); // _ = window // .available_actions(view_id, &cx) @@ -276,7 +288,7 @@ impl PickerDelegate for CommandPaletteDelegate { picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.actions = actions; + delegate.commands = actions; delegate.matches = matches; if delegate.matches.is_empty() { delegate.selected_ix = 0; @@ -290,32 +302,25 @@ impl PickerDelegate for CommandPaletteDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - dbg!("dismissed"); self.command_palette - .update(cx, |command_palette, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .log_err(); } fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - // if !self.matches.is_empty() { - // let window = cx.window(); - // let focused_view_id = self.focused_view_id; - // let action_ix = self.matches[self.selected_ix].candidate_id; - // let command = self.actions.remove(action_ix); - // cx.update_default_global(|hit_counts: &mut HitCounts, _| { - // *hit_counts.0.entry(command.name).or_default() += 1; - // }); - // let action = command.action; - - // cx.app_context() - // .spawn(move |mut cx| async move { - // window - // .dispatch_action(focused_view_id, action.as_ref(), &mut cx) - // .ok_or_else(|| anyhow!("window was closed")) - // }) - // .detach_and_log_err(cx); - // } - self.dismissed(cx) + 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( @@ -324,7 +329,26 @@ impl PickerDelegate for CommandPaletteDelegate { selected: bool, cx: &mut ViewContext>, ) -> Self::ListItem { - div().child("ooh yeah") + 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( diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 8dbe989b1f..3e77a66936 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -4149,16 +4149,12 @@ fn build_key_listeners( build_key_listener( move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { if phase == DispatchPhase::Bubble { - dbg!(&dispatch_context); if let KeyMatch::Some(action) = cx.match_keystroke( &global_element_id, &key_down.keystroke, dispatch_context, ) { - dbg!("got action", &action); return Some(action); - } else { - dbg!("not action"); } } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 3a1832e58c..170ddf942f 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -104,7 +104,17 @@ 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! { @@ -114,7 +124,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, - builders_by_type_id: HashMap, + names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -123,20 +133,22 @@ 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.builders_by_type_id.insert(TypeId::of::(), 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 build_action = lock - .builders_by_type_id + let name = lock + .names_by_type_id .get(type_id) - .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?; + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + drop(lock); - (build_action)(None) + build_action(&name, None) } /// Construct an action based on its name and optional JSON parameters sourced from the keymap. diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 51efde62c1..a546c1b40b 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -414,7 +414,6 @@ pub trait ElementInteractivity: 'static { Box::new(move |_, key_down, context, phase, cx| { if phase == DispatchPhase::Bubble { let key_down = key_down.downcast_ref::().unwrap(); - dbg!(&context); if let KeyMatch::Some(action) = cx.match_keystroke(&global_id, &key_down.keystroke, context) { diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 67041dc488..829f7a3b2c 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -44,19 +44,6 @@ impl KeyBinding { pending_keystrokes: &[Keystroke], contexts: &[&DispatchContext], ) -> KeyMatch { - let should_debug = self.keystrokes.len() == 1 - && self.keystrokes[0].key == "p" - && self.keystrokes[0].modifiers.command == true - && self.keystrokes[0].modifiers.shift == true; - - if false && should_debug { - dbg!( - &self.keystrokes, - &pending_keystrokes, - &contexts, - &self.matches_context(contexts) - ); - } if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) { diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c86b65c47e..c2033a9595 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -46,7 +46,6 @@ impl KeyMatcher { keystroke: &Keystroke, context_stack: &[&DispatchContext], ) -> KeyMatch { - dbg!(keystroke, &context_stack); let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. if keymap.version() != self.keymap_version { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..ffea7c4517 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -145,7 +145,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, + pub initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -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 123a516b02..6a464a4554 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -228,7 +228,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, @@ -327,7 +327,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, @@ -407,6 +407,9 @@ impl<'a> WindowContext<'a> { } 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), @@ -428,6 +431,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) { @@ -1055,6 +1066,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) { @@ -1197,7 +1228,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; @@ -1224,7 +1255,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 { @@ -1296,11 +1330,9 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn available_actions(&mut self) -> Vec> { - let key_dispatch_stack = &self.window.current_frame.key_dispatch_stack; - let mut actions = Vec::new(); - dbg!(key_dispatch_stack.len()); - for frame in key_dispatch_stack { + 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 { @@ -1308,21 +1340,19 @@ impl<'a> WindowContext<'a> { listener: _, } => { match build_action_from_type(event_type) { - Ok(action) => { - actions.push(action); - } + Ok(action) => Some(action), Err(err) => { dbg!(err); + None } // we'll hit his if TypeId == KeyDown } } - KeyDispatchStackFrame::Context(_) => {} + KeyDispatchStackFrame::Context(_) => None, } - } - actions + }) } - fn dispatch_action( + 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 90e1ae931c..3c4d21ad50 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 075cf10ff6..2651d3a190 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, @@ -133,7 +135,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 +148,33 @@ 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().max_h_96().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(), + ), ) } } From ff15ddf3e0c65a6ab565c76e4a87e6376266589e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 16:36:36 -0700 Subject: [PATCH 05/10] Render more than one item --- .../command_palette2/src/command_palette.rs | 67 ++++----------- crates/gpui2/src/elements/uniform_list.rs | 81 +++++++++++++++---- crates/gpui2/src/view.rs | 2 +- 3 files changed, 82 insertions(+), 68 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 6816ecf3a2..fda3dfa8b7 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -147,6 +147,7 @@ impl PickerDelegate for CommandPaletteDelegate { type ListItem = Div>; fn match_count(&self) -> usize { + dbg!(self.matches.len()); self.matches.len() } @@ -163,44 +164,11 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let view_id = &self.previous_focus_handle; - let window = cx.window(); + let mut commands = self.commands.clone(); + cx.spawn(move |picker, mut cx| async move { - let mut actions = picker - .update(&mut cx, |this, _| this.delegate.commands.clone()) - .expect("todo: handle picker no longer being around"); - // _ = window - // .available_actions(view_id, &cx) - // .into_iter() - // .flatten() - // .filter_map(|(name, action, bindings)| { - // let filtered = cx.read(|cx| { - // if cx.has_global::() { - // let filter = cx.global::(); - // filter.filtered_namespaces.contains(action.namespace()) - // } else { - // false - // } - // }); - - // if filtered { - // None - // } else { - // Some(Command { - // name: humanize_action_name(name), - // action, - // keystrokes: bindings - // .iter() - // .map(|binding| binding.keystrokes()) - // .last() - // .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()), - // }) - // } - // }) - // .collect::>(); - cx.read_global::(|hit_counts, _| { - actions.sort_by_key(|action| { + commands.sort_by_key(|action| { ( Reverse(hit_counts.0.get(&action.name).cloned()), action.name.clone(), @@ -209,7 +177,7 @@ impl PickerDelegate for CommandPaletteDelegate { }) .ok(); - let candidates = actions + let candidates = commands .iter() .enumerate() .map(|(ix, command)| StringMatchCandidate { @@ -240,15 +208,13 @@ impl PickerDelegate for CommandPaletteDelegate { ) .await }; - let mut intercept_result = None; - // todo!() for vim mode - // cx.read(|cx| { - // if cx.has_global::() { - // cx.global::()(&query, cx) - // } else { - // None - // } - // }); + + 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 { @@ -266,11 +232,11 @@ impl PickerDelegate for CommandPaletteDelegate { { if let Some(idx) = matches .iter() - .position(|m| actions[m.candidate_id].action.type_id() == action.type_id()) + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) { matches.remove(idx); } - actions.push(Command { + commands.push(Command { name: string.clone(), action, keystrokes: vec![], @@ -278,7 +244,7 @@ impl PickerDelegate for CommandPaletteDelegate { matches.insert( 0, StringMatch { - candidate_id: actions.len() - 1, + candidate_id: commands.len() - 1, string, positions, score: 0.0, @@ -288,7 +254,8 @@ impl PickerDelegate for CommandPaletteDelegate { picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - delegate.commands = actions; + dbg!(&matches); + delegate.commands = commands; delegate.matches = matches; if delegate.matches.is_empty() { delegate.selected_ix = 0; diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index e116022763..151696b8c9 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, }; @@ -86,8 +86,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 +101,49 @@ 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 => item_size.width, + AvailableSpace::MaxContent => item_size.width, + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent => desired_height, + AvailableSpace::MaxContent => desired_height, + }; + dbg!(known_dimensions, available_space, size(width, height)); + size(width, height) + }, + ) } fn paint( @@ -133,12 +168,15 @@ 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; + dbg!(item_height, padded_bounds); 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.) { @@ -146,7 +184,9 @@ impl Element for UniformList { } else { 0 }; + dbg!(visible_item_count); 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 +230,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 +257,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/view.rs b/crates/gpui2/src/view.rs index ffea7c4517..00e1e55cd5 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -145,7 +145,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - pub initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, + initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } From 77d92ff65a98ea29934aa91c07c8084075a3d678 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 20:58:35 -0700 Subject: [PATCH 06/10] Tidy up --- .../command_palette2/src/command_palette.rs | 15 ++-- crates/go_to_line2/src/go_to_line.rs | 33 ++++---- crates/gpui2/src/elements/uniform_list.rs | 26 ++++-- crates/gpui2/src/window.rs | 5 ++ crates/picker2/src/picker2.rs | 33 +++++--- crates/workspace2/src/modal_layer.rs | 84 ++++++++++++++----- 6 files changed, 134 insertions(+), 62 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index fda3dfa8b7..6fa24b7a2e 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -4,7 +4,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Component, Div, Element, EventEmitter, FocusHandle, Keystroke, ParentElement, Render, StatelessInteractive, - Styled, View, ViewContext, VisualContext, WeakView, + Styled, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; @@ -14,7 +14,7 @@ use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; -use workspace::{ModalEvent, Workspace}; +use workspace::{Modal, ModalEvent, Workspace}; use zed_actions::OpenZedURL; actions!(Toggle); @@ -24,7 +24,7 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _: &mut ViewContext| { - workspace.modal_layer().register_modal(Toggle, |_, cx| { + workspace.modal_layer().register_modal(Toggle, |cx| { let Some(previous_focus_handle) = cx.focused() else { return None; }; @@ -73,7 +73,13 @@ impl CommandPalette { 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; @@ -147,7 +153,6 @@ impl PickerDelegate for CommandPaletteDelegate { type ListItem = Div>; fn match_count(&self) -> usize { - dbg!(self.matches.len()); self.matches.len() } @@ -254,7 +259,6 @@ impl PickerDelegate for CommandPaletteDelegate { picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; - dbg!(&matches); delegate.commands = commands; delegate.matches = matches; if delegate.matches.is_empty() { @@ -269,6 +273,7 @@ impl PickerDelegate for CommandPaletteDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { + cx.focus(&self.previous_focus_handle); self.command_palette .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .log_err(); diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c65373e6ac..ca68a9ae79 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -8,22 +8,24 @@ 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::())?; + |workspace: &mut Workspace, cx: &mut ViewContext| { + let handle = cx.view().downgrade(); - Some(cx.build_view(|cx| GoToLine::new(editor, cx))) - }); + workspace.modal_layer().register_modal(Toggle, move |cx| { + let workspace = handle.upgrade()?; + let editor = workspace + .read(cx) + .active_item(cx) + .and_then(|active_item| active_item.downcast::())?; + + Some(cx.build_view(|cx| GoToLine::new(editor, cx))) + }); }, ) .detach(); @@ -44,14 +46,15 @@ pub enum Event { impl EventEmitter for 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 - }); + 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); diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 151696b8c9..181803e1e6 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -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) @@ -123,6 +129,7 @@ impl Element for UniformList { 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, @@ -132,15 +139,12 @@ impl Element for UniformList { .width .unwrap_or(match available_space.width { AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent => item_size.width, - AvailableSpace::MaxContent => item_size.width, + AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, }); let height = match available_space.height { AvailableSpace::Definite(x) => desired_height.min(x), - AvailableSpace::MinContent => desired_height, - AvailableSpace::MaxContent => desired_height, + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, }; - dbg!(known_dimensions, available_space, size(width, height)); size(width, height) }, ) @@ -171,7 +175,6 @@ impl Element for UniformList { let item_height = self .measure_first_item(view_state, Some(padded_bounds.size.width), cx) .height; - dbg!(item_height, padded_bounds); if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.lock().replace(ScrollHandleState { item_height, @@ -184,7 +187,6 @@ impl Element for UniformList { } else { 0 }; - dbg!(visible_item_count); let scroll_offset = element_state .interactive .scroll_offset() @@ -289,3 +291,11 @@ impl Component for UniformList { AnyElement::new(self) } } + +#[cfg(test)] +mod test { + use crate::{self as gpui, TestAppContext}; + + #[gpui::test] + fn test_uniform_list(cx: &mut TestAppContext) {} +} diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6a464a4554..0e60e28dc1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -146,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) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 2651d3a190..9d0019b2dc 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -59,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(); } } @@ -69,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(); } } @@ -77,6 +79,7 @@ impl Picker { if count > 0 { self.delegate.set_selected_index(0, cx); self.scroll_handle.scroll_to_item(0); + cx.notify(); } } @@ -85,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(); } } @@ -163,18 +167,23 @@ impl Render for Picker { .bg(cx.theme().colors().element_background), ) .child( - v_stack().py_0p5().px_1().grow().max_h_96().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() + .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 fc85ae8351..8a3f724972 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,14 +1,21 @@ use crate::Workspace; use gpui::{ - div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatefulInteractivity, - StatelessInteractive, Styled, Subscription, View, ViewContext, + div, px, AnyView, Component, Div, EventEmitter, FocusHandle, ParentElement, Render, + StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, + 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, + active_modal: Option, registered_modals: Vec<( TypeId, Box< @@ -19,6 +26,10 @@ pub struct ModalLayer { )>, } +pub trait Modal: Render + EventEmitter { + fn focus(&self, cx: &mut WindowContext); +} + pub enum ModalEvent { Dismissed, } @@ -26,16 +37,15 @@ pub enum ModalEvent { impl ModalLayer { pub fn new() -> Self { Self { - open_modal: None, - subscription: None, + active_modal: None, registered_modals: Vec::new(), } } pub fn register_modal(&mut self, action: A, build_view: B) where - V: EventEmitter + Render, - B: Fn(&mut Workspace, &mut ViewContext) -> Option> + 'static, + V: Modal, + B: Fn(&mut WindowContext) -> Option> + 'static, { let build_view = Arc::new(build_view); @@ -45,29 +55,56 @@ impl ModalLayer { let build_view = build_view.clone(); div.on_action(move |workspace, event: &A, cx| { - let Some(new_modal) = (build_view)(workspace, cx) else { + let previous_focus = cx.focused(); + if let Some(active_modal) = &workspace.modal_layer().active_modal { + if active_modal.modal.clone().downcast::().is_ok() { + workspace.modal_layer().hide_modal(cx); + return; + } + } + let Some(new_modal) = (build_view)(cx) else { return; }; - workspace.modal_layer().show_modal(new_modal, cx); + workspace + .modal_layer() + .show_modal(previous_focus, new_modal, cx); }) }), )); } - pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) - where + pub fn show_modal( + &mut self, + previous_focus: Option, + new_modal: View, + cx: &mut ViewContext, + ) where V: EventEmitter + Render, { - 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()); + self.active_modal = Some(ActiveModal { + modal: new_modal.clone().into(), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { + ModalEvent::Dismissed => this.modal_layer().hide_modal(cx), + }), + previous_focus_handle: previous_focus, + focus_handle: cx.focus_handle(), + }); cx.notify(); } pub fn hide_modal(&mut self, cx: &mut ViewContext) { - self.open_modal.take(); - self.subscription.take(); + dbg!("hiding..."); + if let Some(active_modal) = self.active_modal.take() { + dbg!("something"); + if let Some(previous_focus) = active_modal.previous_focus_handle { + dbg!("oohthing"); + if active_modal.focus_handle.contains_focused(cx) { + dbg!("aahthing"); + previous_focus.focus(cx); + } + } + } + cx.notify(); } @@ -81,7 +118,7 @@ impl ModalLayer { parent = (action)(parent); } - parent.when_some(self.open_modal.as_ref(), |parent, open_modal| { + parent.when_some(self.active_modal.as_ref(), |parent, open_modal| { let container1 = div() .absolute() .flex() @@ -92,10 +129,13 @@ impl ModalLayer { .left_0() .z_index(400); - // transparent layer - let container2 = v_stack().h(px(0.0)).relative().top_20(); + let container2 = v_stack() + .h(px(0.0)) + .relative() + .top_20() + .track_focus(&open_modal.focus_handle); - parent.child(container1.child(container2.child(open_modal.clone()))) + parent.child(container1.child(container2.child(open_modal.modal.clone()))) }) } } From e6d6806693c86ae91ec81b1434766cb6af4d4497 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 21:11:10 -0700 Subject: [PATCH 07/10] Tidy up some more modal behaviour --- .../command_palette2/src/command_palette.rs | 6 +-- crates/go_to_line2/src/go_to_line.rs | 4 -- crates/gpui2/src/elements/uniform_list.rs | 8 ---- crates/gpui2/src/interactive.rs | 6 +-- crates/workspace2/src/modal_layer.rs | 44 +++++++++++-------- 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 6fa24b7a2e..77d64d63da 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -65,11 +65,7 @@ impl CommandPalette { let delegate = CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle, cx); - let picker = cx.build_view(|cx| { - let picker = Picker::new(delegate, cx); - picker.focus(cx); - picker - }); + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); Self { picker } } } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index cc41f63718..38b46df4e2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -126,10 +126,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/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 181803e1e6..2fe61f5909 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -291,11 +291,3 @@ impl Component for UniformList { AnyElement::new(self) } } - -#[cfg(test)] -mod test { - use crate::{self as gpui, TestAppContext}; - - #[gpui::test] - fn test_uniform_list(cx: &mut TestAppContext) {} -} diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b..243eb3cb07 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/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 8a3f724972..aa5b2e7848 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -55,31 +55,38 @@ impl ModalLayer { let build_view = build_view.clone(); div.on_action(move |workspace, event: &A, cx| { - let previous_focus = cx.focused(); - if let Some(active_modal) = &workspace.modal_layer().active_modal { - if active_modal.modal.clone().downcast::().is_ok() { - workspace.modal_layer().hide_modal(cx); - return; - } - } - let Some(new_modal) = (build_view)(cx) else { - return; - }; - workspace - .modal_layer() - .show_modal(previous_focus, new_modal, cx); + workspace.modal_layer().toggle_modal(build_view.clone(), cx) }) }), )); } + pub fn toggle_modal(&mut self, build_view: Arc, cx: &mut ViewContext) + where + V: Modal, + B: Fn(&mut WindowContext) -> Option> + 'static, + { + let previous_focus = cx.focused(); + + if let Some(active_modal) = &self.active_modal { + if active_modal.modal.clone().downcast::().is_ok() { + self.hide_modal(cx); + return; + } + } + let Some(new_modal) = (build_view)(cx) else { + return; + }; + self.show_modal(previous_focus, new_modal, cx); + } + pub fn show_modal( &mut self, previous_focus: Option, new_modal: View, cx: &mut ViewContext, ) where - V: EventEmitter + Render, + V: Modal, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), @@ -93,13 +100,9 @@ impl ModalLayer { } pub fn hide_modal(&mut self, cx: &mut ViewContext) { - dbg!("hiding..."); if let Some(active_modal) = self.active_modal.take() { - dbg!("something"); if let Some(previous_focus) = active_modal.previous_focus_handle { - dbg!("oohthing"); if active_modal.focus_handle.contains_focused(cx) { - dbg!("aahthing"); previous_focus.focus(cx); } } @@ -133,7 +136,10 @@ impl ModalLayer { .h(px(0.0)) .relative() .top_20() - .track_focus(&open_modal.focus_handle); + .track_focus(&open_modal.focus_handle) + .on_mouse_down_out(|workspace: &mut Workspace, _, cx| { + workspace.modal_layer().hide_modal(cx); + }); parent.child(container1.child(container2.child(open_modal.modal.clone()))) }) From d4b1d1b52881f387afc00a18742d999b36372604 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 21:51:48 -0700 Subject: [PATCH 08/10] Move from register_modals to register_workspace_action --- .../command_palette2/src/command_palette.rs | 23 +++--- crates/go_to_line2/src/go_to_line.rs | 30 ++++---- crates/workspace2/src/modal_layer.rs | 70 ++++--------------- crates/workspace2/src/workspace2.rs | 58 +++++++++++---- 4 files changed, 84 insertions(+), 97 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 77d64d63da..3b3a6684d5 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -21,19 +21,7 @@ actions!(Toggle); pub fn init(cx: &mut AppContext) { cx.set_global(HitCounts::default()); - - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace.modal_layer().register_modal(Toggle, |cx| { - let Some(previous_focus_handle) = cx.focused() else { - return None; - }; - - Some(cx.build_view(|cx| CommandPalette::new(previous_focus_handle, cx))) - }); - }, - ) - .detach(); + cx.observe_new_views(CommandPalette::register).detach(); } pub struct CommandPalette { @@ -41,6 +29,15 @@ pub struct CommandPalette { } 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::(); diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 38b46df4e2..9ec770e05c 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -13,22 +13,7 @@ use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, cx: &mut ViewContext| { - let handle = cx.view().downgrade(); - - workspace.modal_layer().register_modal(Toggle, move |cx| { - let workspace = handle.upgrade()?; - let editor = workspace - .read(cx) - .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 { @@ -47,6 +32,19 @@ impl Modal for GoToLine { } impl GoToLine { + 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); diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index aa5b2e7848..22fc2cd6b9 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -2,7 +2,7 @@ use crate::Workspace; use gpui::{ div, px, AnyView, Component, Div, EventEmitter, FocusHandle, ParentElement, Render, StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, - WindowContext, + VisualContext, WindowContext, }; use std::{any::TypeId, sync::Arc}; use ui::v_stack; @@ -16,14 +16,6 @@ pub struct ActiveModal { pub struct ModalLayer { active_modal: Option, - registered_modals: Vec<( - TypeId, - Box< - dyn Fn( - Div>, - ) -> Div>, - >, - )>, } pub trait Modal: Render + EventEmitter { @@ -36,35 +28,13 @@ pub enum ModalEvent { impl ModalLayer { pub fn new() -> Self { - Self { - active_modal: 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: Modal, - B: Fn(&mut WindowContext) -> Option> + 'static, - { - 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| { - workspace.modal_layer().toggle_modal(build_view.clone(), cx) - }) - }), - )); - } - - pub fn toggle_modal(&mut self, build_view: Arc, cx: &mut ViewContext) - where - V: Modal, - B: Fn(&mut WindowContext) -> Option> + 'static, + B: FnOnce(&mut ViewContext) -> V, { let previous_focus = cx.focused(); @@ -74,28 +44,23 @@ impl ModalLayer { return; } } - let Some(new_modal) = (build_view)(cx) else { - return; - }; - self.show_modal(previous_focus, new_modal, cx); + let new_modal = cx.build_view(build_view); + self.show_modal(new_modal, cx); } - pub fn show_modal( - &mut self, - previous_focus: Option, - new_modal: View, - cx: &mut ViewContext, - ) where + pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) + where V: Modal, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), - subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.modal_layer().hide_modal(cx), + subscription: cx.subscribe(&new_modal, |workspace, modal, e, cx| match e { + ModalEvent::Dismissed => workspace.modal_layer.hide_modal(cx), }), - previous_focus_handle: previous_focus, + previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); + new_modal.update(cx, |modal, cx| modal.focus(cx)); cx.notify(); } @@ -115,12 +80,7 @@ impl ModalLayer { &self, cx: &ViewContext, ) -> Div> { - let mut parent = div().id("modal layer").relative().size_full(); - - for (_, action) in self.registered_modals.iter() { - parent = (action)(parent); - } - + let parent = div().id("boop"); parent.when_some(self.active_modal.as_ref(), |parent, open_modal| { let container1 = div() .absolute() @@ -137,8 +97,8 @@ impl ModalLayer { .relative() .top_20() .track_focus(&open_modal.focus_handle) - .on_mouse_down_out(|workspace: &mut Workspace, _, cx| { - workspace.modal_layer().hide_modal(cx); + .on_mouse_down_out(|workspace: &mut Workspace, event, cx| { + workspace.modal_layer.hide_modal(cx); }); parent.child(container1.child(container2.child(open_modal.modal.clone()))) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 54c8709d7e..7e43941af8 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, DispatchContext, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatefulInteractivity, 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, @@ -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,34 @@ 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.toggle_modal(cx, build) + } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { @@ -3706,14 +3739,13 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = DispatchContext::default(); context.insert("Workspace"); cx.with_key_dispatch_context(context, |cx| { div() - .id("workspace") .relative() .size_full() .flex() @@ -3727,8 +3759,7 @@ impl Render for Workspace { .child(self.render_titlebar(cx)) .child( // todo! should this be a component a view? - self.modal_layer - .wrapper_element(cx) + self.add_workspace_actions_listeners(div().id("workspace")) .relative() .flex_1() .w_full() @@ -3737,6 +3768,7 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) + .child(self.modal_layer.wrapper_element(cx)) // .children( // Some( // Panel::new("project-panel-outer", cx) From 5a711886d4562d31f16818c514d983804a275581 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 22:11:11 -0700 Subject: [PATCH 09/10] Refactor to make ModalLayer a View --- .../command_palette2/src/command_palette.rs | 244 +++++++++--------- crates/workspace2/src/modal_layer.rs | 94 +++---- crates/workspace2/src/workspace2.rs | 9 +- 3 files changed, 162 insertions(+), 185 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 3b3a6684d5..385a3c875e 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,10 +1,9 @@ -use anyhow::anyhow; use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, Action, AnyElement, AnyWindowHandle, AppContext, BorrowWindow, Component, Div, - Element, EventEmitter, FocusHandle, Keystroke, ParentElement, Render, StatelessInteractive, - Styled, View, ViewContext, VisualContext, WeakView, WindowContext, + 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}; @@ -60,7 +59,7 @@ impl CommandPalette { .collect(); let delegate = - CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle, cx); + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); let picker = cx.build_view(|cx| Picker::new(delegate, cx)); Self { picker } @@ -125,17 +124,20 @@ impl CommandPaletteDelegate { command_palette: WeakView, commands: Vec, previous_focus_handle: FocusHandle, - cx: &ViewContext, ) -> 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, - matches: vec![StringMatch { - candidate_id: 0, - score: 0., - positions: vec![], - string: "Foo my bar".into(), - }], selected_ix: 0, previous_focus_handle, } @@ -405,129 +407,129 @@ impl std::fmt::Debug for Command { } } -#[cfg(test)] -mod tests { - use std::sync::Arc; +// #[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}; +// 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" - ); - } +// #[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); +// #[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 - }); +// 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| { +// cx.focus(&editor); +// workspace.add_item(Box::new(editor.clone()), cx) +// }); - workspace.update(cx, |workspace, cx| { - toggle_command_palette(workspace, &Toggle, cx); - }); +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); - let palette = workspace.read_with(cx, |workspace, _| { - workspace.modal::().unwrap() - }); +// 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, 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, _| { +// 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| { +// 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"); - }); +// 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"); - }) - }); +// // 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); - }); +// 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() - }); +// // 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, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; - palette.update(cx, |palette, _| { - assert!(palette.delegate().matches.is_empty()) - }); - } +// 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 - }) - } -} +// 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/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 22fc2cd6b9..bffeec6c56 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,10 +1,7 @@ -use crate::Workspace; use gpui::{ - div, px, AnyView, Component, Div, EventEmitter, FocusHandle, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, - VisualContext, WindowContext, + 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 { @@ -31,7 +28,7 @@ impl ModalLayer { Self { active_modal: None } } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where V: Modal, B: FnOnce(&mut ViewContext) -> V, @@ -48,14 +45,14 @@ impl ModalLayer { 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: Modal, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), - subscription: cx.subscribe(&new_modal, |workspace, modal, e, cx| match e { - ModalEvent::Dismissed => workspace.modal_layer.hide_modal(cx), + 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(), @@ -64,7 +61,7 @@ impl ModalLayer { cx.notify(); } - pub fn hide_modal(&mut self, cx: &mut ViewContext) { + 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) { @@ -75,57 +72,34 @@ impl ModalLayer { cx.notify(); } - - pub fn wrapper_element( - &self, - cx: &ViewContext, - ) -> Div> { - let parent = div().id("boop"); - parent.when_some(self.active_modal.as_ref(), |parent, open_modal| { - let container1 = div() - .absolute() - .flex() - .flex_col() - .items_center() - .size_full() - .top_0() - .left_0() - .z_index(400); - - let container2 = v_stack() - .h(px(0.0)) - .relative() - .top_20() - .track_focus(&open_modal.focus_handle) - .on_mouse_down_out(|workspace: &mut Workspace, event, cx| { - workspace.modal_layer.hide_modal(cx); - }); - - parent.child(container1.child(container2.child(open_modal.modal.clone()))) - }) - } } -// impl Render for ModalLayer { -// type Element = Div; +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(); -// }), -// ) -// } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let Some(active_modal) = &self.active_modal else { + return div(); + }; -// 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 7e43941af8..5c678df317 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -550,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, @@ -702,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, _| { @@ -3526,7 +3526,8 @@ impl Workspace { where B: FnOnce(&mut ViewContext) -> V, { - self.modal_layer.toggle_modal(cx, build) + self.modal_layer + .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) } } @@ -3768,7 +3769,7 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) - .child(self.modal_layer.wrapper_element(cx)) + .child(self.modal_layer.clone()) // .children( // Some( // Panel::new("project-panel-outer", cx) From cc9fb9dea0817f6a3392b22e78a4e12eee2a9501 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 9 Nov 2023 22:23:36 -0700 Subject: [PATCH 10/10] Fix panic caused by focusing the same thing twice --- crates/command_palette2/src/command_palette.rs | 1 - crates/gpui2/src/window.rs | 4 ++++ crates/workspace2/src/modal_layer.rs | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 385a3c875e..abba09519b 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -268,7 +268,6 @@ impl PickerDelegate for CommandPaletteDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.focus(&self.previous_focus_handle); self.command_palette .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .log_err(); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0e60e28dc1..b020366ad0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -407,6 +407,10 @@ 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); } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index bffeec6c56..09ffa6c13f 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -36,8 +36,9 @@ impl ModalLayer { let previous_focus = cx.focused(); if let Some(active_modal) = &self.active_modal { - if active_modal.modal.clone().downcast::().is_ok() { - self.hide_modal(cx); + let is_close = active_modal.modal.clone().downcast::().is_ok(); + self.hide_modal(cx); + if is_close { return; } }