From e34a488b55cb4e3d08f5f02e98691206b2a8a430 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 18 Oct 2023 20:58:24 -0400 Subject: [PATCH 01/20] WIP --- crates/ui2/src/components/workspace.rs | 226 +++++++++++++++---------- 1 file changed, 135 insertions(+), 91 deletions(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index b198861e9538f9cc46f8072df1325779ead434df..fc385542d054283df2aea1269073bc53ceaddb2f 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -1,13 +1,31 @@ use chrono::DateTime; use gpui3::{px, relative, view, Context, Size, View}; -use crate::prelude::*; +use crate::settings::Settings; +use crate::{h_stack, prelude::*, Button}; use crate::{ theme, v_stack, AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label, LanguageSelector, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, Toast, ToastOrigin, }; +#[derive(Clone)] +pub struct GPUI2UIDebug { + pub in_livestream: bool, + pub enable_user_settings: bool, + pub show_toast: bool, +} + +impl Default for GPUI2UIDebug { + fn default() -> Self { + Self { + in_livestream: false, + enable_user_settings: false, + show_toast: false, + } + } +} + #[derive(Clone)] pub struct Workspace { title_bar: View, @@ -18,11 +36,14 @@ pub struct Workspace { show_assistant_panel: bool, show_notifications_panel: bool, show_terminal: bool, + show_debug: bool, show_language_selector: bool, left_panel_scroll_state: ScrollState, right_panel_scroll_state: ScrollState, tab_bar_scroll_state: ScrollState, bottom_panel_scroll_state: ScrollState, + debug: GPUI2UIDebug, + settings: Settings, } impl Workspace { @@ -36,11 +57,14 @@ impl Workspace { show_assistant_panel: false, show_terminal: true, show_language_selector: false, + show_debug: false, show_notifications_panel: true, left_panel_scroll_state: ScrollState::default(), right_panel_scroll_state: ScrollState::default(), tab_bar_scroll_state: ScrollState::default(), bottom_panel_scroll_state: ScrollState::default(), + debug: GPUI2UIDebug::default(), + settings: Settings::default(), } } @@ -122,6 +146,21 @@ impl Workspace { cx.notify(); } + pub fn toggle_debug(&mut self, cx: &mut ViewContext) { + self.show_debug = !self.show_debug; + + cx.notify(); + } + + pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext) { + if self.debug.enable_user_settings { + self.debug.enable_user_settings = false; + } else { + self.debug.enable_user_settings = true; + } + cx.notify(); + } + pub fn view(cx: &mut WindowContext) -> View { view(cx.entity(|cx| Self::new(cx)), Self::render) } @@ -141,74 +180,77 @@ impl Workspace { SplitDirection::Horizontal, ); - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans Extended") - .gap_0() - .justify_start() - .items_start() - .text_color(theme.lowest.base.default.foreground) - .bg(theme.lowest.base.default.background) - .child(self.title_bar.clone()) + v_stack() .child( div() - .flex_1() - .w_full() + .relative() + .size_full() .flex() - .flex_row() - .overflow_hidden() - .border_t() - .border_b() - .border_color(theme.lowest.base.default.border) - .children( - Some( - Panel::new(self.left_panel_scroll_state.clone()) - .side(PanelSide::Left) - .child(ProjectPanel::new(ScrollState::default())), - ) - .filter(|_| self.is_project_panel_open()), - ) - .children( - Some( - Panel::new(self.left_panel_scroll_state.clone()) - .child(CollabPanel::new(ScrollState::default())) - .side(PanelSide::Left), - ) - .filter(|_| self.is_collab_panel_open()), - ) + .flex_col() + .font("Zed Sans Extended") + .gap_0() + .justify_start() + .items_start() + .text_color(theme.lowest.base.default.foreground) + .bg(theme.lowest.base.default.background) + .child(self.title_bar.clone()) .child( - v_stack() + div() .flex_1() - .h_full() + .w_full() + .flex() + .flex_row() + .overflow_hidden() + .border_t() + .border_b() + .border_color(theme.lowest.base.default.border) + .children( + Some( + Panel::new(self.left_panel_scroll_state.clone()) + .side(PanelSide::Left) + .child(ProjectPanel::new(ScrollState::default())), + ) + .filter(|_| self.is_project_panel_open()), + ) + .children( + Some( + Panel::new(self.left_panel_scroll_state.clone()) + .child(CollabPanel::new(ScrollState::default())) + .side(PanelSide::Left), + ) + .filter(|_| self.is_collab_panel_open()), + ) .child( - div() - .flex() + v_stack() .flex_1() - // CSS Hack: Flex 1 has to have a set height to properly fill the space - // Or it will give you a height of 0 - // Marshall: We may not need this anymore with `gpui3`. It seems to render - // fine without it. - .h_px() - .child(root_group), + .h_full() + .child( + div() + .flex() + .flex_1() + // CSS Hack: Flex 1 has to have a set height to properly fill the space + // Or it will give you a height of 0 + // Marshall: We may not need this anymore with `gpui3`. It seems to render + // fine without it. + .h_px() + .child(root_group), + ) + .children( + Some( + Panel::new(self.bottom_panel_scroll_state.clone()) + .child(Terminal::new()) + .allowed_sides(PanelAllowedSides::BottomOnly) + .side(PanelSide::Bottom), + ) + .filter(|_| self.is_terminal_open()), + ), ) .children( Some( - Panel::new(self.bottom_panel_scroll_state.clone()) - .child(Terminal::new()) - .allowed_sides(PanelAllowedSides::BottomOnly) - .side(PanelSide::Bottom), - ) - .filter(|_| self.is_terminal_open()), - ), - ) - .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .side(PanelSide::Right) - .child(ChatPanel::new(ScrollState::default()).messages(vec![ + Panel::new(self.right_panel_scroll_state.clone()) + .side(PanelSide::Right) + .child(ChatPanel::new(ScrollState::default()).messages( + vec![ ChatMessage::new( "osiewicz".to_string(), "is this thing on?".to_string(), @@ -223,45 +265,47 @@ impl Workspace { .unwrap() .naive_local(), ), - ])), - ) - .filter(|_| self.is_chat_panel_open()), + ], + )), + ) + .filter(|_| self.is_chat_panel_open()), + ) + .children( + Some( + Panel::new(self.right_panel_scroll_state.clone()) + .side(PanelSide::Right) + .child(div().w_96().h_full().child("Notifications")), + ) + .filter(|_| self.is_notifications_panel_open()), + ) + .children( + Some( + Panel::new(self.right_panel_scroll_state.clone()) + .child(AssistantPanel::new()), + ) + .filter(|_| self.is_assistant_panel_open()), + ), ) + .child(StatusBar::new()) .children( Some( - Panel::new(self.right_panel_scroll_state.clone()) - .side(PanelSide::Right) - .child(div().w_96().h_full().child("Notifications")), + div() + .absolute() + .top(px(50.)) + .left(px(640.)) + .z_index(999) + .child(LanguageSelector::new()), ) - .filter(|_| self.is_notifications_panel_open()), + .filter(|_| self.is_language_selector_open()), ) - .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .child(AssistantPanel::new()), - ) - .filter(|_| self.is_assistant_panel_open()), - ), + .child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))), ) - .child(StatusBar::new()) - .children( - Some( - div() - .absolute() - .top(px(50.)) - .left(px(640.)) - .z_index(999) - .child(LanguageSelector::new()), - ) - .filter(|_| self.is_language_selector_open()), + .child( + h_stack().gap_2().child( + Button::::new("Toggle Debug") + .on_click(|workspace, cx| workspace.toggle_debug(cx)), + ), ) - .child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // .child(Toast::new(ToastOrigin::BottomRight).child(Label::new("Another toast"))) - // .child(NotificationToast::new( - // "Can't pull changes from origin", - // "Your local branch is behind the remote branch. Please pull the latest changes before pushing.", - // Button::new("Stash & Switch").variant(ButtonVariant::Filled), - // ).secondary_action(Button::new("Cancel"))) } } From f5c76d93bce0f0864535b8d8c91b85e1e668efa8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 10:25:54 -0400 Subject: [PATCH 02/20] Add missing `Arc` for `on_click` handler --- crates/ui2/src/components/workspace.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index fc385542d054283df2aea1269073bc53ceaddb2f..13e652791d07c6b14897c80a8ced40a259cc6d1d 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use chrono::DateTime; use gpui3::{px, relative, view, Context, Size, View}; @@ -303,7 +305,7 @@ impl Workspace { .child( h_stack().gap_2().child( Button::::new("Toggle Debug") - .on_click(|workspace, cx| workspace.toggle_debug(cx)), + .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), ), ) } From 8e465b439301ff55ea004f9ca93a5f3681af71c0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 19 Oct 2023 12:09:39 -0400 Subject: [PATCH 03/20] Add basic debug panel --- crates/ui2/src/components/workspace.rs | 223 ++++++++++++++----------- 1 file changed, 126 insertions(+), 97 deletions(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 13e652791d07c6b14897c80a8ced40a259cc6d1d..a4e4f079d50467a3fc7264766fc3e8307e6e769e 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -4,7 +4,7 @@ use chrono::DateTime; use gpui3::{px, relative, view, Context, Size, View}; use crate::settings::Settings; -use crate::{h_stack, prelude::*, Button}; +use crate::{prelude::*, Button}; use crate::{ theme, v_stack, AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label, LanguageSelector, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, @@ -163,6 +163,24 @@ impl Workspace { cx.notify(); } + pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext) { + if self.debug.in_livestream { + self.debug.in_livestream = false; + } else { + self.debug.in_livestream = true; + } + cx.notify(); + } + + pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext) { + if self.debug.show_toast { + self.debug.show_toast = false; + } else { + self.debug.show_toast = true; + } + cx.notify(); + } + pub fn view(cx: &mut WindowContext) -> View { view(cx.entity(|cx| Self::new(cx)), Self::render) } @@ -182,77 +200,64 @@ impl Workspace { SplitDirection::Horizontal, ); - v_stack() + div() + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans Extended") + .gap_0() + .justify_start() + .items_start() + .text_color(theme.lowest.base.default.foreground) + .bg(theme.lowest.base.default.background) + .child(self.title_bar.clone()) .child( div() - .relative() - .size_full() + .flex_1() + .w_full() .flex() - .flex_col() - .font("Zed Sans Extended") - .gap_0() - .justify_start() - .items_start() - .text_color(theme.lowest.base.default.foreground) - .bg(theme.lowest.base.default.background) - .child(self.title_bar.clone()) + .flex_row() + .overflow_hidden() + .border_t() + .border_b() + .border_color(theme.lowest.base.default.border) + .children( + Some( + Panel::new(self.left_panel_scroll_state.clone()) + .side(PanelSide::Left) + .child(ProjectPanel::new(ScrollState::default())), + ) + .filter(|_| self.is_project_panel_open()), + ) + .children( + Some( + Panel::new(self.left_panel_scroll_state.clone()) + .child(CollabPanel::new(ScrollState::default())) + .side(PanelSide::Left), + ) + .filter(|_| self.is_collab_panel_open()), + ) .child( - div() + v_stack() .flex_1() - .w_full() - .flex() - .flex_row() - .overflow_hidden() - .border_t() - .border_b() - .border_color(theme.lowest.base.default.border) + .h_full() + .child(div().flex().flex_1().child(root_group)) .children( Some( - Panel::new(self.left_panel_scroll_state.clone()) - .side(PanelSide::Left) - .child(ProjectPanel::new(ScrollState::default())), + Panel::new(self.bottom_panel_scroll_state.clone()) + .child(Terminal::new()) + .allowed_sides(PanelAllowedSides::BottomOnly) + .side(PanelSide::Bottom), ) - .filter(|_| self.is_project_panel_open()), - ) - .children( - Some( - Panel::new(self.left_panel_scroll_state.clone()) - .child(CollabPanel::new(ScrollState::default())) - .side(PanelSide::Left), - ) - .filter(|_| self.is_collab_panel_open()), - ) - .child( - v_stack() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_1() - // CSS Hack: Flex 1 has to have a set height to properly fill the space - // Or it will give you a height of 0 - // Marshall: We may not need this anymore with `gpui3`. It seems to render - // fine without it. - .h_px() - .child(root_group), - ) - .children( - Some( - Panel::new(self.bottom_panel_scroll_state.clone()) - .child(Terminal::new()) - .allowed_sides(PanelAllowedSides::BottomOnly) - .side(PanelSide::Bottom), - ) - .filter(|_| self.is_terminal_open()), - ), - ) - .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .side(PanelSide::Right) - .child(ChatPanel::new(ScrollState::default()).messages( - vec![ + .filter(|_| self.is_terminal_open()), + ), + ) + .children( + Some( + Panel::new(self.right_panel_scroll_state.clone()) + .side(PanelSide::Right) + .child(ChatPanel::new(ScrollState::default()).messages(vec![ ChatMessage::new( "osiewicz".to_string(), "is this thing on?".to_string(), @@ -267,46 +272,70 @@ impl Workspace { .unwrap() .naive_local(), ), - ], - )), - ) - .filter(|_| self.is_chat_panel_open()), - ) - .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .side(PanelSide::Right) - .child(div().w_96().h_full().child("Notifications")), - ) - .filter(|_| self.is_notifications_panel_open()), - ) - .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .child(AssistantPanel::new()), - ) - .filter(|_| self.is_assistant_panel_open()), - ), + ])), + ) + .filter(|_| self.is_chat_panel_open()), ) - .child(StatusBar::new()) .children( Some( - div() - .absolute() - .top(px(50.)) - .left(px(640.)) - .z_index(999) - .child(LanguageSelector::new()), + Panel::new(self.right_panel_scroll_state.clone()) + .side(PanelSide::Right) + .child(div().w_96().h_full().child("Notifications")), ) - .filter(|_| self.is_language_selector_open()), + .filter(|_| self.is_notifications_panel_open()), ) - .child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))), + .children( + Some( + Panel::new(self.right_panel_scroll_state.clone()) + .child(AssistantPanel::new()), + ) + .filter(|_| self.is_assistant_panel_open()), + ), + ) + .child(StatusBar::new()) + .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()), + ) + .filter(|_| self.is_language_selector_open()), ) + .z_index(8) + // Debug .child( - h_stack().gap_2().child( - Button::::new("Toggle Debug") - .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - ), + v_stack() + .z_index(9) + .absolute() + .bottom_10() + .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))), + ), ) } } From 61e09ff532187ef90d30841a057a94104daaddb9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 12:58:17 -0400 Subject: [PATCH 04/20] Checkpoint: Thread `WindowContext` through to `user_settings` --- crates/storybook2/src/storybook2.rs | 29 +++++++++++++------- crates/ui2/src/components/assistant_panel.rs | 2 +- crates/ui2/src/components/chat_panel.rs | 13 ++++----- crates/ui2/src/components/icon_button.rs | 4 +-- crates/ui2/src/components/list.rs | 4 +-- crates/ui2/src/components/panel.rs | 10 +++---- crates/ui2/src/components/project_panel.rs | 2 +- crates/ui2/src/components/title_bar.rs | 4 +-- crates/ui2/src/components/workspace.rs | 25 +++++++---------- crates/ui2/src/elements/button.rs | 4 +-- crates/ui2/src/elements/icon.rs | 4 +-- crates/ui2/src/elements/label.rs | 2 +- crates/ui2/src/lib.rs | 8 ++++++ crates/ui2/src/prelude.rs | 6 ++-- crates/ui2/src/settings.rs | 18 ++++++------ 15 files changed, 71 insertions(+), 64 deletions(-) diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 490006ecb9ad2182f30f1ee7d734da6fe3ca0de7..32bc45ae5844fa5d259087bafa1f10789ce3cd16 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -17,7 +17,7 @@ use log::LevelFilter; use simplelog::SimpleLogger; use story_selector::ComponentStory; use ui::prelude::*; -use ui::themed; +use ui::{themed, FakeSettings}; use crate::assets::Assets; use crate::story_selector::StorySelector; @@ -68,8 +68,10 @@ fn main() { move |cx| { view( cx.entity(|cx| { - cx.with_global(theme.clone(), |cx| { - StoryWrapper::new(selector.story(cx), theme) + cx.with_global(FakeSettings::default(), |cx| { + cx.with_global(theme.clone(), |cx| { + StoryWrapper::new(selector.story(cx), theme) + }) }) }), StoryWrapper::render, @@ -85,20 +87,27 @@ fn main() { pub struct StoryWrapper { story: AnyView, theme: Theme, + settings: FakeSettings, } impl StoryWrapper { pub(crate) fn new(story: AnyView, theme: Theme) -> Self { - Self { story, theme } + Self { + story, + theme, + settings: FakeSettings::default(), + } } fn render(&mut self, cx: &mut ViewContext) -> impl Element { - themed(self.theme.clone(), cx, |cx| { - div() - .flex() - .flex_col() - .size_full() - .child(self.story.clone()) + cx.with_global(self.settings.clone(), |cx| { + themed(self.theme.clone(), cx, |cx| { + div() + .flex() + .flex_col() + .size_full() + .child(self.story.clone()) + }) }) } } diff --git a/crates/ui2/src/components/assistant_panel.rs b/crates/ui2/src/components/assistant_panel.rs index b64f7befd30ff7f4ee45442234f3c6dc744a9433..e6b099f338753e870c8b25133e259dfaadaaf7ea 100644 --- a/crates/ui2/src/components/assistant_panel.rs +++ b/crates/ui2/src/components/assistant_panel.rs @@ -29,7 +29,7 @@ impl AssistantPanel { fn render(&mut self, view: &mut S, cx: &mut ViewContext) -> impl Element { let theme = theme(cx); - Panel::new(self.scroll_state.clone()) + Panel::new(cx) .children(vec![div() .flex() .flex_col() diff --git a/crates/ui2/src/components/chat_panel.rs b/crates/ui2/src/components/chat_panel.rs index 45a62e207a264ce1eff21ae59bb1f311135c96cd..81a039a6ad7afc1fca37baffe9d3b999593f66ba 100644 --- a/crates/ui2/src/components/chat_panel.rs +++ b/crates/ui2/src/components/chat_panel.rs @@ -138,13 +138,10 @@ mod stories { Story::container(cx) .child(Story::title_for::<_, ChatPanel>(cx)) .child(Story::label(cx, "Default")) - .child( - Panel::new(ScrollState::default()) - .child(ChatPanel::new(ScrollState::default())), - ) + .child(Panel::new(cx).child(ChatPanel::new(ScrollState::default()))) .child(Story::label(cx, "With Mesages")) - .child(Panel::new(ScrollState::default()).child( - ChatPanel::new(ScrollState::default()).messages(vec![ + .child( + Panel::new(cx).child(ChatPanel::new(ScrollState::default()).messages(vec![ ChatMessage::new( "osiewicz".to_string(), "is this thing on?".to_string(), @@ -159,8 +156,8 @@ mod stories { .unwrap() .naive_local(), ), - ]), - )) + ])), + ) } } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 3f41b4f5b953e89f54db58fa97aefcfcfbc3c4f1..ebe7d29e58a0559c8ea1f24c4ad2bdb720758f5d 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -78,8 +78,8 @@ impl IconButton { let mut button = h_stack() .justify_center() .rounded_md() - .py(ui_size(0.25)) - .px(ui_size(6. / 14.)) + .py(ui_size(cx, 0.25)) + .px(ui_size(cx, 6. / 14.)) .when(self.variant == ButtonVariant::Filled, |this| { this.bg(color.filled_element) }) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 1a60581855aae9f5052c703d3b1d94c82d1bddb1..ce7977e7ea29ce6a2054605e2efd284258a37091 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -363,7 +363,7 @@ impl ListEntry { let theme = theme(cx); let system_color = SystemColor::new(); let color = ThemeColor::new(cx); - let setting = user_settings(); + let settings = user_settings(cx); let left_content = match self.left_content.clone() { Some(LeftContent::Icon(i)) => Some( @@ -395,7 +395,7 @@ impl ListEntry { // .ml(rems(0.75 * self.indent_level as f32)) .children((0..self.indent_level).map(|_| { div() - .w(*setting.list_indent_depth) + .w(*settings.list_indent_depth) .h_full() .flex() .justify_center() diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 52e36b2f14e21c4c2f61683d8d8b0ce10efeadc3..53851e1433102de8166058bc01f7793dfbf7c5cf 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -53,15 +53,15 @@ pub struct Panel { } impl Panel { - pub fn new(scroll_state: ScrollState) -> Self { - let setting = user_settings(); + pub fn new(cx: &mut WindowContext) -> Self { + let settings = user_settings(cx); Self { state_type: PhantomData, - scroll_state, + scroll_state: ScrollState::default(), current_side: PanelSide::default(), allowed_sides: PanelAllowedSides::default(), - initial_width: *setting.default_panel_size, + initial_width: *settings.default_panel_size, width: None, children: SmallVec::new(), } @@ -175,7 +175,7 @@ mod stories { .child(Story::title_for::<_, Panel>(cx)) .child(Story::label(cx, "Default")) .child( - Panel::new(ScrollState::default()).child( + Panel::new(cx).child( div() .overflow_y_scroll(ScrollState::default()) .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))), diff --git a/crates/ui2/src/components/project_panel.rs b/crates/ui2/src/components/project_panel.rs index 4c4fbe9f293602bd4dfe8435837203ccee50df22..480ae7f2c0de9cef18711ba774cde791b6911bec 100644 --- a/crates/ui2/src/components/project_panel.rs +++ b/crates/ui2/src/components/project_panel.rs @@ -87,7 +87,7 @@ mod stories { .child(Story::title_for::<_, ProjectPanel>(cx)) .child(Story::label(cx, "Default")) .child( - Panel::new(ScrollState::default()) + Panel::new(cx) .child(ProjectPanel::new(ScrollState::default())), ) } diff --git a/crates/ui2/src/components/title_bar.rs b/crates/ui2/src/components/title_bar.rs index 68033629ba0f9dbe69a4aeda0c9e42cddbde2330..4472c8cc6e7e0eec34b94316e42bb23d4859f82c 100644 --- a/crates/ui2/src/components/title_bar.rs +++ b/crates/ui2/src/components/title_bar.rs @@ -95,7 +95,7 @@ impl TitleBar { fn render(&mut self, cx: &mut ViewContext) -> impl Element { let theme = theme(cx); let color = ThemeColor::new(cx); - let setting = user_settings(); + let settings = user_settings(cx); // let has_focus = cx.window_is_active(); let has_focus = true; @@ -127,7 +127,7 @@ impl TitleBar { .flex() .items_center() .gap_1() - .when(*setting.titlebar.show_project_owner, |this| { + .when(*settings.titlebar.show_project_owner, |this| { this.child(Button::new("iamnbutler")) }) .child(Button::new("zed")) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index b198861e9538f9cc46f8072df1325779ead434df..96cb4090463aaf5708e7e57f80137c68973eb3c7 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -165,7 +165,7 @@ impl Workspace { .border_color(theme.lowest.base.default.border) .children( Some( - Panel::new(self.left_panel_scroll_state.clone()) + Panel::new(cx) .side(PanelSide::Left) .child(ProjectPanel::new(ScrollState::default())), ) @@ -173,7 +173,7 @@ impl Workspace { ) .children( Some( - Panel::new(self.left_panel_scroll_state.clone()) + Panel::new(cx) .child(CollabPanel::new(ScrollState::default())) .side(PanelSide::Left), ) @@ -196,7 +196,7 @@ impl Workspace { ) .children( Some( - Panel::new(self.bottom_panel_scroll_state.clone()) + Panel::new(cx) .child(Terminal::new()) .allowed_sides(PanelAllowedSides::BottomOnly) .side(PanelSide::Bottom), @@ -205,10 +205,8 @@ impl Workspace { ), ) .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .side(PanelSide::Right) - .child(ChatPanel::new(ScrollState::default()).messages(vec![ + Some(Panel::new(cx).side(PanelSide::Right).child( + ChatPanel::new(ScrollState::default()).messages(vec![ ChatMessage::new( "osiewicz".to_string(), "is this thing on?".to_string(), @@ -223,24 +221,21 @@ impl Workspace { .unwrap() .naive_local(), ), - ])), - ) + ]), + )) .filter(|_| self.is_chat_panel_open()), ) .children( Some( - Panel::new(self.right_panel_scroll_state.clone()) + Panel::new(cx) .side(PanelSide::Right) .child(div().w_96().h_full().child("Notifications")), ) .filter(|_| self.is_notifications_panel_open()), ) .children( - Some( - Panel::new(self.right_panel_scroll_state.clone()) - .child(AssistantPanel::new()), - ) - .filter(|_| self.is_assistant_panel_open()), + Some(Panel::new(cx).child(AssistantPanel::new())) + .filter(|_| self.is_assistant_panel_open()), ), ) .child(StatusBar::new()) diff --git a/crates/ui2/src/elements/button.rs b/crates/ui2/src/elements/button.rs index 3491352116a6aff076f111e67f8391aaaa78e44e..7d7040e18154ad7f185140430bc8ab63dfead727 100644 --- a/crates/ui2/src/elements/button.rs +++ b/crates/ui2/src/elements/button.rs @@ -149,11 +149,11 @@ impl Button { fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { let icon_color = self.icon_color(); let border_color = self.border_color(cx); - let setting = user_settings(); + let settings = user_settings(cx); let mut el = h_stack() .p_1() - .text_size(ui_size(1.)) + .text_size(ui_size(cx, 1.)) .rounded_md() .border() .border_color(border_color) diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/elements/icon.rs index 00c22432d47110dce99dac7df785b6638125c6e5..000863a399762f7e19608bc2b26d358382b928a8 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/elements/icon.rs @@ -180,8 +180,8 @@ impl IconElement { let theme = theme(cx); let fill = self.color.color(theme); let svg_size = match self.size { - IconSize::Small => ui_size(12. / 14.), - IconSize::Medium => ui_size(15. / 14.), + IconSize::Small => ui_size(cx, 12. / 14.), + IconSize::Medium => ui_size(cx, 15. / 14.), }; svg() diff --git a/crates/ui2/src/elements/label.rs b/crates/ui2/src/elements/label.rs index 37cabc553490e3e70def4a014999bed779020e70..95c7e86a102a58d6da1ed07c5315a28199d7bcaf 100644 --- a/crates/ui2/src/elements/label.rs +++ b/crates/ui2/src/elements/label.rs @@ -96,7 +96,7 @@ impl Label { .bg(LabelColor::Hidden.hsla(cx)), ) }) - .text_size(ui_size(1.)) + .text_size(ui_size(cx, 1.)) .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) diff --git a/crates/ui2/src/lib.rs b/crates/ui2/src/lib.rs index 417a77fabe59b1e89794765b06ce9531c679ad10..10419ed362b7732e568e7f8dbe4cc18d7d36c9e8 100644 --- a/crates/ui2/src/lib.rs +++ b/crates/ui2/src/lib.rs @@ -14,6 +14,14 @@ pub use elements::*; pub use prelude::*; pub use static_data::*; +// This needs to be fully qualified with `crate::` otherwise we get a panic +// at: +// thread '' panicked at crates/gpui3/src/platform/mac/platform.rs:66:81: +// called `Option::unwrap()` on a `None` value +// +// AFAICT this is something to do with conflicting names between crates and modules that +// interfaces with declaring the `ClassDecl`. +pub use crate::settings::*; pub use crate::theme::*; #[cfg(feature = "stories")] diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index a132542e299b4e088449e215f262df0e42f93ac9..ef7e0fc5ab60e8b33859b52d468f5388ce4fd506 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -142,12 +142,12 @@ impl HighlightColor { } } -pub fn ui_size(size: f32) -> Rems { +pub fn ui_size(cx: &mut WindowContext, size: f32) -> Rems { const UI_SCALE_RATIO: f32 = 0.875; - let setting = user_settings(); + let settings = user_settings(cx); - rems(*setting.ui_scale * UI_SCALE_RATIO * size) + rems(*settings.ui_scale * UI_SCALE_RATIO * size) } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] diff --git a/crates/ui2/src/settings.rs b/crates/ui2/src/settings.rs index e91ff4a961cfdf1e5e04cfb01ffcd3e057849540..fd7693fd08209ab92199959c99422aea47122c92 100644 --- a/crates/ui2/src/settings.rs +++ b/crates/ui2/src/settings.rs @@ -1,15 +1,13 @@ use std::ops::Deref; -use gpui3::{rems, AbsoluteLength}; +use gpui3::{rems, AbsoluteLength, WindowContext}; use crate::DisclosureControlStyle; -// This is a fake static example of user settings overriding the default settings -pub fn user_settings() -> Settings { - let mut settings = Settings::default(); - settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into()); - // settings.ui_scale = SettingValue::UserDefined(2.); - settings +/// Returns the user settings. +pub fn user_settings(cx: &WindowContext) -> FakeSettings { + // cx.global::().clone() + FakeSettings::default() } #[derive(Clone)] @@ -48,7 +46,7 @@ impl Default for TitlebarSettings { // These should be merged into settings #[derive(Clone)] -pub struct Settings { +pub struct FakeSettings { pub default_panel_size: SettingValue, pub list_disclosure_style: SettingValue, pub list_indent_depth: SettingValue, @@ -56,7 +54,7 @@ pub struct Settings { pub ui_scale: SettingValue, } -impl Default for Settings { +impl Default for FakeSettings { fn default() -> Self { Self { titlebar: TitlebarSettings::default(), @@ -68,4 +66,4 @@ impl Default for Settings { } } -impl Settings {} +impl FakeSettings {} From a1f7a97ff5c3c53f8eafb52830bea1c38383523e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 13:02:32 -0400 Subject: [PATCH 05/20] Pull the settings from the global state --- crates/storybook2/src/storybook2.rs | 4 +- crates/ui2/src/settings.rs | 84 +++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 32bc45ae5844fa5d259087bafa1f10789ce3cd16..4beca32ca18377c948d44ac8d84b65801b237a88 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -17,7 +17,7 @@ use log::LevelFilter; use simplelog::SimpleLogger; use story_selector::ComponentStory; use ui::prelude::*; -use ui::{themed, FakeSettings}; +use ui::{themed, with_settings, FakeSettings}; use crate::assets::Assets; use crate::story_selector::StorySelector; @@ -100,7 +100,7 @@ impl StoryWrapper { } fn render(&mut self, cx: &mut ViewContext) -> impl Element { - cx.with_global(self.settings.clone(), |cx| { + with_settings(self.settings.clone(), cx, |cx| { themed(self.theme.clone(), cx, |cx| { div() .flex() diff --git a/crates/ui2/src/settings.rs b/crates/ui2/src/settings.rs index fd7693fd08209ab92199959c99422aea47122c92..05df5b49566c9c4d9f97b6d0bbd70e4c483df904 100644 --- a/crates/ui2/src/settings.rs +++ b/crates/ui2/src/settings.rs @@ -1,13 +1,14 @@ use std::ops::Deref; -use gpui3::{rems, AbsoluteLength, WindowContext}; +use gpui3::{ + rems, AbsoluteLength, AnyElement, BorrowAppContext, Bounds, LayoutId, Pixels, WindowContext, +}; -use crate::DisclosureControlStyle; +use crate::prelude::*; /// Returns the user settings. pub fn user_settings(cx: &WindowContext) -> FakeSettings { - // cx.global::().clone() - FakeSettings::default() + cx.global::().clone() } #[derive(Clone)] @@ -67,3 +68,78 @@ impl Default for FakeSettings { } impl FakeSettings {} + +pub fn with_settings( + settings: FakeSettings, + cx: &mut ViewContext, + build_child: F, +) -> WithSettings +where + E: Element, + F: FnOnce(&mut ViewContext) -> E, +{ + let child = cx.with_global(theme.clone(), |cx| build_child(cx)); + WithSettings { settings, child } +} + +pub struct WithSettings { + pub(crate) settings: FakeSettings, + pub(crate) child: E, +} + +impl IntoAnyElement for WithSettings +where + E: Element, +{ + fn into_any(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl Element for WithSettings { + type ViewState = E::ViewState; + type ElementState = E::ElementState; + + fn id(&self) -> Option { + None + } + + fn initialize( + &mut self, + view_state: &mut Self::ViewState, + element_state: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + cx.with_global(self.settings.clone(), |cx| { + self.child.initialize(view_state, element_state, cx) + }) + } + + fn layout( + &mut self, + view_state: &mut E::ViewState, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId + where + Self: Sized, + { + cx.with_global(self.settings.clone(), |cx| { + self.child.layout(view_state, element_state, cx) + }) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut Self::ViewState, + frame_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) where + Self: Sized, + { + cx.with_global(self.settings.clone(), |cx| { + self.child.paint(bounds, view_state, frame_state, cx); + }); + } +} From 4aac733238dd05f7e77aeb9c65ce509e1de24427 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 13:23:08 -0400 Subject: [PATCH 06/20] Pass the settings to `build_child` --- crates/ui2/src/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui2/src/settings.rs b/crates/ui2/src/settings.rs index 05df5b49566c9c4d9f97b6d0bbd70e4c483df904..ffc2ef785183ce64044ec6733902fa903f61fe70 100644 --- a/crates/ui2/src/settings.rs +++ b/crates/ui2/src/settings.rs @@ -78,7 +78,7 @@ where E: Element, F: FnOnce(&mut ViewContext) -> E, { - let child = cx.with_global(theme.clone(), |cx| build_child(cx)); + let child = cx.with_global(settings.clone(), |cx| build_child(cx)); WithSettings { settings, child } } From a869de3b1f1750e83d2097cecb83ad0762510448 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 13:38:19 -0400 Subject: [PATCH 07/20] Add ability to toggle user settings --- crates/ui2/src/components/workspace.rs | 37 ++++++++++++++++---------- crates/ui2/src/settings.rs | 37 +++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index baef402de8ce9c38a7c3a58b7f170274f1e54131..856244f6a1799697444f7b49f18c68b4e943b3a9 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use chrono::DateTime; -use gpui3::{px, relative, view, Context, Size, View}; +use gpui3::{px, relative, rems, view, Context, Size, View}; -use crate::settings::Settings; -use crate::{prelude::*, Button}; +use crate::settings::FakeSettings; +use crate::{prelude::*, user_settings_mut, Button, SettingValue}; use crate::{ theme, v_stack, AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label, LanguageSelector, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, @@ -12,13 +12,13 @@ use crate::{ }; #[derive(Clone)] -pub struct GPUI2UIDebug { +pub struct Gpui2UiDebug { pub in_livestream: bool, pub enable_user_settings: bool, pub show_toast: bool, } -impl Default for GPUI2UIDebug { +impl Default for Gpui2UiDebug { fn default() -> Self { Self { in_livestream: false, @@ -44,8 +44,7 @@ pub struct Workspace { right_panel_scroll_state: ScrollState, tab_bar_scroll_state: ScrollState, bottom_panel_scroll_state: ScrollState, - debug: GPUI2UIDebug, - settings: Settings, + debug: Gpui2UiDebug, } impl Workspace { @@ -65,8 +64,7 @@ impl Workspace { right_panel_scroll_state: ScrollState::default(), tab_bar_scroll_state: ScrollState::default(), bottom_panel_scroll_state: ScrollState::default(), - debug: GPUI2UIDebug::default(), - settings: Settings::default(), + debug: Gpui2UiDebug::default(), } } @@ -155,11 +153,8 @@ impl Workspace { } pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext) { - if self.debug.enable_user_settings { - self.debug.enable_user_settings = false; - } else { - self.debug.enable_user_settings = true; - } + self.debug.enable_user_settings = !self.debug.enable_user_settings; + cx.notify(); } @@ -188,6 +183,20 @@ impl Workspace { pub fn render(&mut self, cx: &mut ViewContext) -> impl Element { let theme = theme(cx).clone(); + // HACK: This should happen inside of `debug_toggle_user_settings`, but + // we don't have `cx.global::()` in event handlers at the moment. + // Need to talk with Nathan/Antonio about this. + { + let settings = user_settings_mut(cx); + + if self.debug.enable_user_settings { + settings.list_indent_depth = SettingValue::UserDefined(rems(0.5).into()); + settings.ui_scale = SettingValue::UserDefined(1.25); + } else { + *settings = FakeSettings::default(); + } + } + let root_group = PaneGroup::new_panes( vec![Pane::new( ScrollState::default(), diff --git a/crates/ui2/src/settings.rs b/crates/ui2/src/settings.rs index ffc2ef785183ce64044ec6733902fa903f61fe70..995da199cf90a6b600e9fc14a9eaaa2bbe2cf3b0 100644 --- a/crates/ui2/src/settings.rs +++ b/crates/ui2/src/settings.rs @@ -1,7 +1,9 @@ use std::ops::Deref; +use std::sync::Arc; use gpui3::{ - rems, AbsoluteLength, AnyElement, BorrowAppContext, Bounds, LayoutId, Pixels, WindowContext, + rems, AbsoluteLength, AnyElement, BorrowAppContext, Bounds, Interactive, LayoutId, Pixels, + WindowContext, }; use crate::prelude::*; @@ -11,6 +13,10 @@ pub fn user_settings(cx: &WindowContext) -> FakeSettings { cx.global::().clone() } +pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings { + cx.global_mut::() +} + #[derive(Clone)] pub enum SettingValue { UserDefined(T), @@ -143,3 +149,32 @@ impl Element for WithSettings { }); } } + +impl Interactive for WithSettings { + fn listeners(&mut self) -> &mut gpui3::EventListeners { + self.child.listeners() + } + + fn on_mouse_down( + mut self, + button: gpui3::MouseButton, + handler: impl Fn(&mut Self::ViewState, &gpui3::MouseDownEvent, &mut ViewContext) + + Send + + Sync + + 'static, + ) -> Self + where + Self: Sized, + { + println!("WithSettings on_mouse_down"); + + let settings = self.settings.clone(); + + self.listeners() + .mouse_down + .push(Arc::new(move |view, event, bounds, phase, cx| { + cx.with_global(settings.clone(), |cx| handler(view, event, cx)) + })); + self + } +} From 9985f388ac2c9d32dfd35f1e8a4367ff630df573 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 17:19:25 +0200 Subject: [PATCH 08/20] Checkpoint --- crates/gpui3/src/elements/div.rs | 32 +++++-- crates/gpui3/src/events.rs | 11 ++- crates/gpui3/src/focus.rs | 52 +---------- crates/gpui3/src/gpui3.rs | 4 +- crates/gpui3/src/interactive.rs | 73 ++++++++++++++- crates/gpui3/src/keymap.rs | 16 ++++ crates/gpui3/src/window.rs | 156 +++++++++++++++---------------- 7 files changed, 203 insertions(+), 141 deletions(-) create mode 100644 crates/gpui3/src/keymap.rs diff --git a/crates/gpui3/src/elements/div.rs b/crates/gpui3/src/elements/div.rs index 76561f742b8f3a3ce12318fee4cea366c6d7d5e6..37c07e2559a6e1a01681913884b37d9201c60b28 100644 --- a/crates/gpui3/src/elements/div.rs +++ b/crates/gpui3/src/elements/div.rs @@ -1,15 +1,16 @@ use crate::{ Active, Anonymous, AnyElement, AppContext, BorrowWindow, Bounds, Click, DispatchPhase, Element, ElementFocusability, ElementId, ElementIdentity, EventListeners, Focus, FocusHandle, Focusable, - Hover, Identified, Interactive, IntoAnyElement, LayoutId, MouseClickEvent, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, NonFocusable, Overflow, ParentElement, Pixels, Point, - ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, ViewContext, + Hover, Identified, Interactive, IntoAnyElement, KeyDownEvent, KeyMatch, LayoutId, + MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, NonFocusable, Overflow, + ParentElement, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, + ViewContext, }; use collections::HashMap; use parking_lot::Mutex; use refineable::Refineable; use smallvec::SmallVec; -use std::{mem, sync::Arc}; +use std::{any::TypeId, mem, sync::Arc}; #[derive(Default)] pub struct DivState { @@ -423,11 +424,30 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { + let element_state = element_state.unwrap_or_default(); for listener in self.listeners.focus.iter().cloned() { cx.on_focus_changed(move |view, event, cx| listener(view, event, cx)); } - let key_listeners = mem::take(&mut self.listeners.key); + let mut key_listeners = mem::take(&mut self.listeners.key); + + if let Some(id) = self.id() { + key_listeners.push(( + TypeId::of::(), + Arc::new(move |_, key_down, phase, cx| { + if phase == DispatchPhase::Bubble { + let key_down = key_down.downcast_ref::().unwrap(); + if let KeyMatch::Some(action) = cx.match_keystroke(&id, &key_down.keystroke) + { + return Some(action); + } + } + + None + }), + )); + } + cx.with_key_listeners(&key_listeners, |cx| { if let Some(focus_handle) = self.focusability.focus_handle().cloned() { cx.with_focus(focus_handle, |cx| { @@ -443,7 +463,7 @@ where }); self.listeners.key = key_listeners; - element_state.unwrap_or_default() + element_state } fn layout( diff --git a/crates/gpui3/src/events.rs b/crates/gpui3/src/events.rs index 58228b4175139d2da20b032e8acb8e24ec1a32d4..e7dc2e5359faee273140589fd129ed8c46c887e4 100644 --- a/crates/gpui3/src/events.rs +++ b/crates/gpui3/src/events.rs @@ -1,5 +1,6 @@ use crate::{ - point, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, ViewContext, + point, AnyBox, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, + ViewContext, }; use smallvec::SmallVec; use std::{ @@ -254,8 +255,12 @@ pub type ScrollWheelListener = Arc< + 'static, >; -pub type KeyListener = - Arc) + Send + Sync + 'static>; +pub type KeyListener = Arc< + dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) -> Option + + Send + + Sync + + 'static, +>; pub type FocusListener = Arc) + Send + Sync + 'static>; diff --git a/crates/gpui3/src/focus.rs b/crates/gpui3/src/focus.rs index ecfda740f2296bc4e2aa24abe54210854793af18..c7cefbe27885f83c91955c06fb1c9fb53dd794cc 100644 --- a/crates/gpui3/src/focus.rs +++ b/crates/gpui3/src/focus.rs @@ -1,9 +1,5 @@ -use std::{any::TypeId, sync::Arc}; - -use crate::{ - DispatchPhase, FocusEvent, FocusHandle, Interactive, KeyDownEvent, KeyUpEvent, StyleRefinement, - ViewContext, -}; +use crate::{FocusEvent, FocusHandle, Interactive, StyleRefinement, ViewContext}; +use std::sync::Arc; pub trait Focus: Interactive { fn set_focus_style(&mut self, style: StyleRefinement); @@ -135,48 +131,4 @@ pub trait Focus: Interactive { })); self } - - fn on_key_down( - mut self, - listener: impl Fn( - &mut Self::ViewState, - &KeyDownEvent, - DispatchPhase, - &mut ViewContext, - ) + Send - + Sync - + 'static, - ) -> Self - where - Self: Sized, - { - self.listeners().key.push(( - TypeId::of::(), - Arc::new(move |view, event, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx) - }), - )); - self - } - - fn on_key_up( - mut self, - listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext) - + Send - + Sync - + 'static, - ) -> Self - where - Self: Sized, - { - self.listeners().key.push(( - TypeId::of::(), - Arc::new(move |view, event, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx) - }), - )); - self - } } diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index caf265a60c8df828308c22524dab7d0eaa612e55..1af9a1cc222386f812fbf1e9c6d535bfafc968e8 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -11,6 +11,7 @@ mod geometry; mod hover; mod image_cache; mod interactive; +mod keymap; mod platform; mod scene; mod style; @@ -38,6 +39,7 @@ pub use gpui3_macros::*; pub use hover::*; pub use image_cache::*; pub use interactive::*; +pub use keymap::*; pub use platform::*; pub use refineable::*; pub use scene::*; @@ -64,7 +66,7 @@ use std::{ }; use taffy::TaffyLayoutEngine; -type AnyBox = Box; +type AnyBox = Box; pub trait Context { type EntityContext<'a, 'w, T: 'static + Send + Sync>; diff --git a/crates/gpui3/src/interactive.rs b/crates/gpui3/src/interactive.rs index ac150652dd1e5d9bbd27c3b6c247c2737954ba8a..9d1197419f114ac2ce1b807bb8cbb1d3ce9f14be 100644 --- a/crates/gpui3/src/interactive.rs +++ b/crates/gpui3/src/interactive.rs @@ -1,8 +1,8 @@ -use std::sync::Arc; +use std::{any::TypeId, sync::Arc}; use crate::{ - DispatchPhase, Element, EventListeners, MouseButton, MouseClickEvent, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext, + DispatchPhase, Element, EventListeners, KeyDownEvent, KeyUpEvent, MouseButton, MouseClickEvent, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, ViewContext, }; pub trait Interactive: Element { @@ -143,6 +143,73 @@ pub trait Interactive: Element { })); self } + + fn on_key_down( + mut self, + listener: impl Fn( + &mut Self::ViewState, + &KeyDownEvent, + DispatchPhase, + &mut ViewContext, + ) + Send + + Sync + + 'static, + ) -> Self + where + Self: Sized, + { + self.listeners().key.push(( + TypeId::of::(), + Arc::new(move |view, event, phase, cx| { + let event = event.downcast_ref().unwrap(); + listener(view, event, phase, cx); + None + }), + )); + self + } + + fn on_key_up( + mut self, + listener: impl Fn(&mut Self::ViewState, &KeyUpEvent, DispatchPhase, &mut ViewContext) + + Send + + Sync + + 'static, + ) -> Self + where + Self: Sized, + { + self.listeners().key.push(( + TypeId::of::(), + Arc::new(move |view, event, phase, cx| { + let event = event.downcast_ref().unwrap(); + listener(view, event, phase, cx); + None + }), + )); + self + } + + fn on_action( + mut self, + listener: impl Fn(&mut Self::ViewState, &A, DispatchPhase, &mut ViewContext) + + Send + + Sync + + 'static, + ) -> Self + where + Self: Sized, + { + self.listeners().key.push(( + TypeId::of::(), + Arc::new(move |view, event, phase, cx| { + let event = event.downcast_ref().unwrap(); + listener(view, event, phase, cx); + None + }), + )); + self + } } pub trait Click: Interactive { diff --git a/crates/gpui3/src/keymap.rs b/crates/gpui3/src/keymap.rs new file mode 100644 index 0000000000000000000000000000000000000000..d5dae0f4a4be7acc86fde013274feb6df53991bd --- /dev/null +++ b/crates/gpui3/src/keymap.rs @@ -0,0 +1,16 @@ +use crate::{AnyBox, Keystroke}; + +#[derive(Default)] +pub struct KeyMatcher; + +impl KeyMatcher { + pub fn push_keystroke(&mut self, keystroke: Keystroke) -> KeyMatch { + todo!() + } +} + +pub enum KeyMatch { + None, + Pending, + Some(AnyBox), +} diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index d16539a369ab3ea748c83fa7ea3b1ad8fcd0ab4d..545c07ed2f8997cb477cd7874ac93f6207aca62c 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -2,11 +2,11 @@ use crate::{ px, size, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, - InputEvent, IsZero, KeyListener, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, - MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, - Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task, - Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, + InputEvent, IsZero, KeyListener, KeyMatch, Keystroke, LayoutId, MainThread, MainThreadOnly, + MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, + PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, + ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, + Task, Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::Result; use collections::HashMap; @@ -45,6 +45,9 @@ pub enum DispatchPhase { } type AnyListener = Arc; +type AnyKeyListener = Arc< + dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) -> Option + Send + Sync + 'static, +>; type AnyFocusListener = Arc; slotmap::new_key_type! { pub struct FocusId; } @@ -146,13 +149,13 @@ pub struct Window { z_index_stack: StackingOrder, content_mask_stack: Vec>, mouse_listeners: HashMap>, - key_listeners: HashMap>, + key_listeners: Vec<(TypeId, AnyKeyListener)>, key_events_enabled: bool, focus_stack: Vec, focus_parents_by_child: HashMap, pub(crate) focus_listeners: Vec, pub(crate) focus_handles: Arc>>, - propagate_event: bool, + propagate: bool, default_prevented: bool, mouse_position: Point, scale_factor: f32, @@ -219,12 +222,12 @@ impl Window { z_index_stack: StackingOrder(SmallVec::new()), content_mask_stack: Vec::new(), mouse_listeners: HashMap::default(), - key_listeners: HashMap::default(), + key_listeners: Vec::new(), key_events_enabled: true, focus_stack: Vec::new(), focus_parents_by_child: HashMap::default(), focus_listeners: Vec::new(), - propagate_event: true, + propagate: true, default_prevented: true, mouse_position, scale_factor, @@ -434,7 +437,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { } pub fn stop_propagation(&mut self) { - self.window.propagate_event = false; + self.window.propagate = false; } pub fn prevent_default(&mut self) { @@ -462,19 +465,6 @@ impl<'a, 'w> WindowContext<'a, 'w> { )) } - pub fn on_keyboard_event( - &mut self, - handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + Send + Sync + 'static, - ) { - self.window - .key_listeners - .entry(TypeId::of::()) - .or_default() - .push(Arc::new(move |event: &dyn Any, phase, cx| { - handler(event.downcast_ref().unwrap(), phase, cx) - })) - } - pub fn mouse_position(&self) -> Point { self.window.mouse_position } @@ -821,7 +811,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { // Clear focus state, because we determine what is focused when the new elements // in the upcoming frame are initialized. window.focus_listeners.clear(); - window.key_listeners.values_mut().for_each(Vec::clear); + window.key_listeners.clear(); window.focus_parents_by_child.clear(); window.key_events_enabled = true; } @@ -837,7 +827,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { } // Handlers may set this to false by calling `stop_propagation` - self.window.propagate_event = true; + self.window.propagate = true; self.window.default_prevented = false; if let Some(mut handlers) = self @@ -852,16 +842,16 @@ impl<'a, 'w> WindowContext<'a, 'w> { // special purposes, such as detecting events outside of a given Bounds. for (_, handler) in &handlers { handler(any_mouse_event, DispatchPhase::Capture, self); - if !self.window.propagate_event { + if !self.window.propagate { break; } } // Bubble phase, where most normal handlers do their work. - if self.window.propagate_event { + if self.window.propagate { for (_, handler) in handlers.iter().rev() { handler(any_mouse_event, DispatchPhase::Bubble, self); - if !self.window.propagate_event { + if !self.window.propagate { break; } } @@ -879,43 +869,68 @@ impl<'a, 'w> WindowContext<'a, 'w> { .mouse_listeners .insert(any_mouse_event.type_id(), handlers); } - } else if let Some(any_keyboard_event) = event.keyboard_event() { - if let Some(mut handlers) = self - .window - .key_listeners - .remove(&any_keyboard_event.type_id()) - { - for handler in &handlers { - handler(any_keyboard_event, DispatchPhase::Capture, self); - if !self.window.propagate_event { + } else if let Some(any_key_event) = event.keyboard_event() { + let key_listeners = mem::take(&mut self.window.key_listeners); + let key_event_type = any_key_event.type_id(); + + for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate() { + if key_event_type == *listener_event_type { + if let Some(action) = listener(any_key_event, DispatchPhase::Capture, self) { + self.dispatch_action(action, &key_listeners[..ix]); + } + if !self.window.propagate { break; } } + } - if self.window.propagate_event { - for handler in handlers.iter().rev() { - handler(any_keyboard_event, DispatchPhase::Bubble, self); - if !self.window.propagate_event { + if self.window.propagate { + for (ix, (listener_event_type, listener)) in key_listeners.iter().enumerate().rev() + { + if key_event_type == *listener_event_type { + if let Some(action) = listener(any_key_event, DispatchPhase::Bubble, self) { + self.dispatch_action(action, &key_listeners[..ix]); + } + + if !self.window.propagate { break; } } } - - handlers.extend( - self.window - .key_listeners - .get_mut(&any_keyboard_event.type_id()) - .into_iter() - .flat_map(|handlers| handlers.drain(..)), - ); - self.window - .key_listeners - .insert(any_keyboard_event.type_id(), handlers); } + + self.window.key_listeners = key_listeners; } true } + + pub fn match_keystroke(&mut self, element_id: &ElementId, keystroke: &Keystroke) -> KeyMatch { + todo!(); + } + + fn dispatch_action(&mut self, action: AnyBox, listeners: &[(TypeId, AnyKeyListener)]) { + let action_type = action.type_id(); + for (event_type, listener) in listeners { + if action_type == *event_type { + listener(action.as_ref(), DispatchPhase::Capture, self); + if !self.window.propagate { + break; + } + } + } + + if self.window.propagate { + for (event_type, listener) in listeners.iter().rev() { + if action_type == *event_type { + listener(action.as_ref(), DispatchPhase::Capture, self); + if !self.window.propagate { + break; + } + } + } + } + } } impl<'a, 'w> MainThread> { @@ -1225,28 +1240,25 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> { f: impl FnOnce(&mut Self) -> R, ) -> R { if self.window.key_events_enabled { - let handle = self.handle(); - for (type_id, listener) in key_listeners { - let handle = handle.clone(); - let listener = listener.clone(); - self.window - .key_listeners - .entry(*type_id) - .or_default() - .push(Arc::new(move |event, phase, cx| { + for (event_type, listener) in key_listeners.iter().cloned() { + let handle = self.handle(); + let listener = Arc::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_, '_>| { handle .update(cx, |view, cx| listener(view, event, phase, cx)) - .log_err(); - })); + .log_err() + .flatten() + }, + ); + self.window.key_listeners.push((event_type, listener)); } } let result = f(self); if self.window.key_events_enabled { - for (type_id, _) in key_listeners { - self.window.key_listeners.get_mut(type_id).unwrap().pop(); - } + let prev_len = self.window.key_listeners.len() - key_listeners.len(); + self.window.key_listeners.truncate(prev_len); } result @@ -1317,18 +1329,6 @@ impl<'a, 'w, V: Send + Sync + 'static> ViewContext<'a, 'w, V> { }) }); } - - pub fn on_keyboard_event( - &mut self, - handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + Send + Sync + 'static, - ) { - let handle = self.handle().upgrade(self).unwrap(); - self.window_cx.on_keyboard_event(move |event, phase, cx| { - handle.update(cx, |view, cx| { - handler(view, event, phase, cx); - }) - }); - } } impl<'a, 'w, S: EventEmitter + Send + Sync + 'static> ViewContext<'a, 'w, S> { From 30269381e875752bab0b107bfabc19d0a0e3e998 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:03:10 +0200 Subject: [PATCH 09/20] Checkpoint --- crates/gpui3/src/element.rs | 2 +- crates/gpui3/src/elements/div.rs | 85 +-- crates/gpui3/src/events.rs | 4 +- crates/gpui3/src/gpui3.rs | 4 +- crates/gpui3/src/keymap.rs | 16 - crates/gpui3/src/keymap_matcher.rs | 488 ++++++++++++++++++ crates/gpui3/src/keymap_matcher/binding.rs | 80 +++ crates/gpui3/src/keymap_matcher/keymap.rs | 400 ++++++++++++++ .../src/keymap_matcher/keymap_context.rs | 325 ++++++++++++ crates/gpui3/src/platform/keystroke.rs | 110 ++-- crates/gpui3/src/platform/mac/events.rs | 1 + crates/gpui3/src/platform/mac/window.rs | 2 + crates/gpui3/src/text_system.rs | 4 +- crates/gpui3/src/text_system/line_layout.rs | 2 +- crates/gpui3/src/view.rs | 6 +- crates/gpui3/src/window.rs | 95 ++-- 16 files changed, 1486 insertions(+), 138 deletions(-) delete mode 100644 crates/gpui3/src/keymap.rs create mode 100644 crates/gpui3/src/keymap_matcher.rs create mode 100644 crates/gpui3/src/keymap_matcher/binding.rs create mode 100644 crates/gpui3/src/keymap_matcher/keymap.rs create mode 100644 crates/gpui3/src/keymap_matcher/keymap_context.rs diff --git a/crates/gpui3/src/element.rs b/crates/gpui3/src/element.rs index 9f5d0078f87b5771ab1a08d08fab96818d2ad4c7..3a9c179cf9355c04bc879da2d36d8a2b60f0e5a8 100644 --- a/crates/gpui3/src/element.rs +++ b/crates/gpui3/src/element.rs @@ -33,7 +33,7 @@ pub trait Element: 'static + Send + Sync + IntoAnyElement { } #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] -pub(crate) struct GlobalElementId(SmallVec<[ElementId; 8]>); +pub struct GlobalElementId(SmallVec<[ElementId; 8]>); pub trait ElementIdentity: 'static + Send + Sync { fn id(&self) -> Option; diff --git a/crates/gpui3/src/elements/div.rs b/crates/gpui3/src/elements/div.rs index 37c07e2559a6e1a01681913884b37d9201c60b28..356e783971dc26929cc60b897df0f409efcb7fa4 100644 --- a/crates/gpui3/src/elements/div.rs +++ b/crates/gpui3/src/elements/div.rs @@ -1,10 +1,10 @@ use crate::{ Active, Anonymous, AnyElement, AppContext, BorrowWindow, Bounds, Click, DispatchPhase, Element, ElementFocusability, ElementId, ElementIdentity, EventListeners, Focus, FocusHandle, Focusable, - Hover, Identified, Interactive, IntoAnyElement, KeyDownEvent, KeyMatch, LayoutId, - MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, NonFocusable, Overflow, - ParentElement, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement, Styled, - ViewContext, + GlobalElementId, Hover, Identified, Interactive, IntoAnyElement, KeyDownEvent, KeyMatch, + LayoutId, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, NonFocusable, + Overflow, ParentElement, Pixels, Point, ScrollWheelEvent, SharedString, Style, StyleRefinement, + Styled, ViewContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -188,12 +188,12 @@ where fn with_element_id( &mut self, cx: &mut ViewContext, - f: impl FnOnce(&mut Self, &mut ViewContext) -> R, + f: impl FnOnce(&mut Self, Option, &mut ViewContext) -> R, ) -> R { if let Some(id) = self.id() { - cx.with_element_id(id, |cx| f(self, cx)) + cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx)) } else { - f(self, cx) + f(self, None, cx) } } @@ -424,46 +424,49 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - let element_state = element_state.unwrap_or_default(); - for listener in self.listeners.focus.iter().cloned() { - cx.on_focus_changed(move |view, event, cx| listener(view, event, cx)); - } - - let mut key_listeners = mem::take(&mut self.listeners.key); + self.with_element_id(cx, |this, global_id, cx| { + let element_state = element_state.unwrap_or_default(); + for listener in this.listeners.focus.iter().cloned() { + cx.on_focus_changed(move |view, event, cx| listener(view, event, cx)); + } - if let Some(id) = self.id() { - key_listeners.push(( - TypeId::of::(), - Arc::new(move |_, key_down, phase, cx| { - if phase == DispatchPhase::Bubble { - let key_down = key_down.downcast_ref::().unwrap(); - if let KeyMatch::Some(action) = cx.match_keystroke(&id, &key_down.keystroke) - { - return Some(action); + let mut key_listeners = mem::take(&mut this.listeners.key); + + if let Some(global_id) = global_id { + key_listeners.push(( + TypeId::of::(), + Arc::new(move |_, key_down, phase, cx| { + if phase == DispatchPhase::Bubble { + let key_down = key_down.downcast_ref::().unwrap(); + if let KeyMatch::Some(action) = + cx.match_keystroke(&global_id, &key_down.keystroke) + { + return Some(action); + } } - } - None - }), - )); - } + None + }), + )); + } - cx.with_key_listeners(&key_listeners, |cx| { - if let Some(focus_handle) = self.focusability.focus_handle().cloned() { - cx.with_focus(focus_handle, |cx| { - for child in &mut self.children { + cx.with_key_listeners(&key_listeners, |cx| { + if let Some(focus_handle) = this.focusability.focus_handle().cloned() { + cx.with_focus(focus_handle, |cx| { + for child in &mut this.children { + child.initialize(view_state, cx); + } + }) + } else { + for child in &mut this.children { child.initialize(view_state, cx); } - }) - } else { - for child in &mut self.children { - child.initialize(view_state, cx); } - } - }); - self.listeners.key = key_listeners; + }); + this.listeners.key = key_listeners; - element_state + element_state + }) } fn layout( @@ -474,7 +477,7 @@ where ) -> LayoutId { let style = self.compute_style(Bounds::default(), element_state, cx); style.apply_text_style(cx, |cx| { - self.with_element_id(cx, |this, cx| { + self.with_element_id(cx, |this, _global_id, cx| { let layout_ids = this .children .iter_mut() @@ -492,7 +495,7 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - self.with_element_id(cx, |this, cx| { + self.with_element_id(cx, |this, _global_id, cx| { if let Some(group) = this.group.clone() { cx.default_global::() .0 diff --git a/crates/gpui3/src/events.rs b/crates/gpui3/src/events.rs index e7dc2e5359faee273140589fd129ed8c46c887e4..10eeb688cc52bd5695ee436683df2e293e87ed57 100644 --- a/crates/gpui3/src/events.rs +++ b/crates/gpui3/src/events.rs @@ -1,5 +1,5 @@ use crate::{ - point, AnyBox, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, + point, Action, Bounds, DispatchPhase, FocusHandle, Keystroke, Modifiers, Pixels, Point, ViewContext, }; use smallvec::SmallVec; @@ -256,7 +256,7 @@ pub type ScrollWheelListener = Arc< >; pub type KeyListener = Arc< - dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) -> Option + dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) -> Option> + Send + Sync + 'static, diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index 1af9a1cc222386f812fbf1e9c6d535bfafc968e8..cdefb94edd1bd4386792013741e9d85a8f03a36a 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -11,7 +11,7 @@ mod geometry; mod hover; mod image_cache; mod interactive; -mod keymap; +mod keymap_matcher; mod platform; mod scene; mod style; @@ -39,7 +39,7 @@ pub use gpui3_macros::*; pub use hover::*; pub use image_cache::*; pub use interactive::*; -pub use keymap::*; +pub use keymap_matcher::*; pub use platform::*; pub use refineable::*; pub use scene::*; diff --git a/crates/gpui3/src/keymap.rs b/crates/gpui3/src/keymap.rs deleted file mode 100644 index d5dae0f4a4be7acc86fde013274feb6df53991bd..0000000000000000000000000000000000000000 --- a/crates/gpui3/src/keymap.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::{AnyBox, Keystroke}; - -#[derive(Default)] -pub struct KeyMatcher; - -impl KeyMatcher { - pub fn push_keystroke(&mut self, keystroke: Keystroke) -> KeyMatch { - todo!() - } -} - -pub enum KeyMatch { - None, - Pending, - Some(AnyBox), -} diff --git a/crates/gpui3/src/keymap_matcher.rs b/crates/gpui3/src/keymap_matcher.rs new file mode 100644 index 0000000000000000000000000000000000000000..955b1890936a1ce74f5c054fd4d481cbddd1524b --- /dev/null +++ b/crates/gpui3/src/keymap_matcher.rs @@ -0,0 +1,488 @@ +mod binding; +mod keymap; +mod keymap_context; + +pub use binding::*; +pub use keymap::*; +pub use keymap_context::*; + +use crate::Keystroke; +use smallvec::SmallVec; +use std::any::{Any, TypeId}; + +pub trait Action: Any + Send + Sync { + fn partial_eq(&self, action: &dyn Action) -> bool; + fn boxed_clone(&self) -> Box; + fn as_any(&self) -> &dyn Any; +} + +pub struct KeymapMatcher { + pending_keystrokes: Vec, + keymap: Keymap, + keymap_version: KeymapVersion, +} + +impl KeymapMatcher { + pub fn new(keymap: Keymap) -> Self { + Self { + pending_keystrokes: Vec::new(), + keymap_version: keymap.version(), + keymap, + } + } + + pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { + self.keymap.bindings_for_action(action_id) + } + + pub fn clear_pending(&mut self) { + self.pending_keystrokes.clear(); + } + + pub fn has_pending_keystrokes(&self) -> bool { + !self.pending_keystrokes.is_empty() + } + + /// Pushes a keystroke onto the matcher. + /// The result of the new keystroke is returned: + /// KeyMatch::None => + /// No match is valid for this key given any pending keystrokes. + /// KeyMatch::Pending => + /// There exist bindings which are still waiting for more keys. + /// KeyMatch::Complete(matches) => + /// One or more bindings have received the necessary key presses. + /// Bindings added later will take precedence over earlier bindings. + pub fn match_keystroke( + &mut self, + keystroke: &Keystroke, + context_stack: &[KeymapContext], + ) -> KeyMatch { + // Clear pending keystrokes if the keymap has changed since the last matched keystroke. + if self.keymap.version() != self.keymap_version { + self.keymap_version = self.keymap.version(); + self.pending_keystrokes.clear(); + } + + let mut pending_key = None; + + for binding in self.keymap.bindings().iter().rev() { + for candidate in keystroke.match_candidates() { + self.pending_keystrokes.push(candidate.clone()); + match binding.match_keystrokes(&self.pending_keystrokes, context_stack) { + KeyMatch::Some(action) => { + self.clear_pending(); + return KeyMatch::Some(action); + } + KeyMatch::Pending => { + pending_key.get_or_insert(candidate); + } + KeyMatch::None => {} + } + self.pending_keystrokes.pop(); + } + } + + if let Some(pending_key) = pending_key { + self.pending_keystrokes.push(pending_key); + } + + if self.pending_keystrokes.is_empty() { + KeyMatch::None + } else { + KeyMatch::Pending + } + } + + pub fn keystrokes_for_action( + &self, + action: &dyn Action, + contexts: &[KeymapContext], + ) -> Option> { + self.keymap + .bindings() + .iter() + .rev() + .find_map(|binding| binding.keystrokes_for_action(action, contexts)) + } +} + +impl Default for KeymapMatcher { + fn default() -> Self { + Self::new(Keymap::default()) + } +} + +pub enum KeyMatch { + None, + Pending, + Some(Box), +} + +impl KeyMatch { + pub fn is_some(&self) -> bool { + matches!(self, KeyMatch::Some(_)) + } +} + +// #[cfg(test)] +// mod tests { +// use anyhow::Result; +// use serde::Deserialize; + +// use crate::{actions, impl_actions, keymap_matcher::KeymapContext}; + +// use super::*; + +// #[test] +// fn test_keymap_and_view_ordering() -> Result<()> { +// actions!(test, [EditorAction, ProjectPanelAction]); + +// let mut editor = KeymapContext::default(); +// editor.add_identifier("Editor"); + +// let mut project_panel = KeymapContext::default(); +// project_panel.add_identifier("ProjectPanel"); + +// // Editor 'deeper' in than project panel +// let dispatch_path = vec![(2, editor), (1, project_panel)]; + +// // But editor actions 'higher' up in keymap +// let keymap = Keymap::new(vec![ +// Binding::new("left", EditorAction, Some("Editor")), +// Binding::new("left", ProjectPanelAction, Some("ProjectPanel")), +// ]); + +// let mut matcher = KeymapMatcher::new(keymap); + +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("left")?, dispatch_path.clone()), +// KeyMatch::Matches(vec![ +// (2, Box::new(EditorAction)), +// (1, Box::new(ProjectPanelAction)), +// ]), +// ); + +// Ok(()) +// } + +// #[test] +// fn test_push_keystroke() -> Result<()> { +// actions!(test, [B, AB, C, D, DA, E, EF]); + +// let mut context1 = KeymapContext::default(); +// context1.add_identifier("1"); + +// let mut context2 = KeymapContext::default(); +// context2.add_identifier("2"); + +// let dispatch_path = vec![(2, context2), (1, context1)]; + +// let keymap = Keymap::new(vec![ +// Binding::new("a b", AB, Some("1")), +// Binding::new("b", B, Some("2")), +// Binding::new("c", C, Some("2")), +// Binding::new("d", D, Some("1")), +// Binding::new("d", D, Some("2")), +// Binding::new("d a", DA, Some("2")), +// ]); + +// let mut matcher = KeymapMatcher::new(keymap); + +// // Binding with pending prefix always takes precedence +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), +// KeyMatch::Pending, +// ); +// // B alone doesn't match because a was pending, so AB is returned instead +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), +// KeyMatch::Matches(vec![(1, Box::new(AB))]), +// ); +// assert!(!matcher.has_pending_keystrokes()); + +// // Without an a prefix, B is dispatched like expected +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("b")?, dispatch_path.clone()), +// KeyMatch::Matches(vec![(2, Box::new(B))]), +// ); +// assert!(!matcher.has_pending_keystrokes()); + +// // If a is prefixed, C will not be dispatched because there +// // was a pending binding for it +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), +// KeyMatch::Pending, +// ); +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("c")?, dispatch_path.clone()), +// KeyMatch::None, +// ); +// assert!(!matcher.has_pending_keystrokes()); + +// // If a single keystroke matches multiple bindings in the tree +// // all of them are returned so that we can fallback if the action +// // handler decides to propagate the action +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("d")?, dispatch_path.clone()), +// KeyMatch::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]), +// ); + +// // If none of the d action handlers consume the binding, a pending +// // binding may then be used +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, dispatch_path.clone()), +// KeyMatch::Matches(vec![(2, Box::new(DA))]), +// ); +// assert!(!matcher.has_pending_keystrokes()); + +// Ok(()) +// } + +// #[test] +// fn test_keystroke_parsing() -> Result<()> { +// assert_eq!( +// Keystroke::parse("ctrl-p")?, +// Keystroke { +// key: "p".into(), +// ctrl: true, +// alt: false, +// shift: false, +// cmd: false, +// function: false, +// ime_key: None, +// } +// ); + +// assert_eq!( +// Keystroke::parse("alt-shift-down")?, +// Keystroke { +// key: "down".into(), +// ctrl: false, +// alt: true, +// shift: true, +// cmd: false, +// function: false, +// ime_key: None, +// } +// ); + +// assert_eq!( +// Keystroke::parse("shift-cmd--")?, +// Keystroke { +// key: "-".into(), +// ctrl: false, +// alt: false, +// shift: true, +// cmd: true, +// function: false, +// ime_key: None, +// } +// ); + +// Ok(()) +// } + +// #[test] +// fn test_context_predicate_parsing() -> Result<()> { +// use KeymapContextPredicate::*; + +// assert_eq!( +// KeymapContextPredicate::parse("a && (b == c || d != e)")?, +// And( +// Box::new(Identifier("a".into())), +// Box::new(Or( +// Box::new(Equal("b".into(), "c".into())), +// Box::new(NotEqual("d".into(), "e".into())), +// )) +// ) +// ); + +// assert_eq!( +// KeymapContextPredicate::parse("!a")?, +// Not(Box::new(Identifier("a".into())),) +// ); + +// Ok(()) +// } + +// #[test] +// fn test_context_predicate_eval() { +// let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap(); + +// let mut context = KeymapContext::default(); +// context.add_identifier("a"); +// assert!(!predicate.eval(&[context])); + +// let mut context = KeymapContext::default(); +// context.add_identifier("a"); +// context.add_identifier("b"); +// assert!(predicate.eval(&[context])); + +// let mut context = KeymapContext::default(); +// context.add_identifier("a"); +// context.add_key("c", "x"); +// assert!(!predicate.eval(&[context])); + +// let mut context = KeymapContext::default(); +// context.add_identifier("a"); +// context.add_key("c", "d"); +// assert!(predicate.eval(&[context])); + +// let predicate = KeymapContextPredicate::parse("!a").unwrap(); +// assert!(predicate.eval(&[KeymapContext::default()])); +// } + +// #[test] +// fn test_context_child_predicate_eval() { +// let predicate = KeymapContextPredicate::parse("a && b > c").unwrap(); +// let contexts = [ +// context_set(&["e", "f"]), +// context_set(&["c", "d"]), // match this context +// context_set(&["a", "b"]), +// ]; + +// assert!(!predicate.eval(&contexts[0..])); +// assert!(predicate.eval(&contexts[1..])); +// assert!(!predicate.eval(&contexts[2..])); + +// let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap(); +// let contexts = [ +// context_set(&["f"]), +// context_set(&["e"]), // only match this context +// context_set(&["c"]), +// context_set(&["a", "b"]), +// context_set(&["e"]), +// context_set(&["c", "d"]), +// context_set(&["a", "b"]), +// ]; + +// assert!(!predicate.eval(&contexts[0..])); +// assert!(predicate.eval(&contexts[1..])); +// assert!(!predicate.eval(&contexts[2..])); +// assert!(!predicate.eval(&contexts[3..])); +// assert!(!predicate.eval(&contexts[4..])); +// assert!(!predicate.eval(&contexts[5..])); +// assert!(!predicate.eval(&contexts[6..])); + +// fn context_set(names: &[&str]) -> KeymapContext { +// let mut keymap = KeymapContext::new(); +// names +// .iter() +// .for_each(|name| keymap.add_identifier(name.to_string())); +// keymap +// } +// } + +// #[test] +// fn test_matcher() -> Result<()> { +// #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] +// pub struct A(pub String); +// impl_actions!(test, [A]); +// actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]); + +// #[derive(Clone, Debug, Eq, PartialEq)] +// struct ActionArg { +// a: &'static str, +// } + +// let keymap = Keymap::new(vec![ +// Binding::new("a", A("x".to_string()), Some("a")), +// Binding::new("b", B, Some("a")), +// Binding::new("a b", Ab, Some("a || b")), +// Binding::new("$", Dollar, Some("a")), +// Binding::new("\"", Quote, Some("a")), +// Binding::new("alt-s", Ess, Some("a")), +// Binding::new("ctrl-`", Backtick, Some("a")), +// ]); + +// let mut context_a = KeymapContext::default(); +// context_a.add_identifier("a"); + +// let mut context_b = KeymapContext::default(); +// context_b.add_identifier("b"); + +// let mut matcher = KeymapMatcher::new(keymap); + +// // Basic match +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))]) +// ); +// matcher.clear_pending(); + +// // Multi-keystroke match +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), +// KeyMatch::Pending +// ); +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Ab))]) +// ); +// matcher.clear_pending(); + +// // Failed matches don't interfere with matching subsequent keys +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]), +// KeyMatch::None +// ); +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(A("x".to_string())))]) +// ); +// matcher.clear_pending(); + +// // Pending keystrokes are cleared when the context changes +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]), +// KeyMatch::Pending +// ); +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]), +// KeyMatch::None +// ); +// matcher.clear_pending(); + +// let mut context_c = KeymapContext::default(); +// context_c.add_identifier("c"); + +// // Pending keystrokes are maintained per-view +// assert_eq!( +// matcher.match_keystroke( +// Keystroke::parse("a")?, +// vec![(1, context_b.clone()), (2, context_c.clone())] +// ), +// KeyMatch::Pending +// ); +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Ab))]) +// ); + +// // handle Czech $ (option + 4 key) +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Dollar))]) +// ); + +// // handle Brazillian quote (quote key then space key) +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Quote))]) +// ); + +// // handle ctrl+` on a brazillian keyboard +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Backtick))]) +// ); + +// // handle alt-s on a US keyboard +// assert_eq!( +// matcher.match_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]), +// KeyMatch::Matches(vec![(1, Box::new(Ess))]) +// ); + +// Ok(()) +// } +// } diff --git a/crates/gpui3/src/keymap_matcher/binding.rs b/crates/gpui3/src/keymap_matcher/binding.rs new file mode 100644 index 0000000000000000000000000000000000000000..e63479ab168f7a119f8957d6dcb8e260def97546 --- /dev/null +++ b/crates/gpui3/src/keymap_matcher/binding.rs @@ -0,0 +1,80 @@ +use crate::{Action, KeyMatch, KeymapContext, KeymapContextPredicate, Keystroke}; +use anyhow::Result; +use smallvec::SmallVec; + +pub struct Binding { + action: Box, + pub(super) keystrokes: SmallVec<[Keystroke; 2]>, + pub(super) context_predicate: Option, +} + +impl Binding { + pub fn new(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self { + Self::load(keystrokes, Box::new(action), context_predicate).unwrap() + } + + pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { + let context = if let Some(context) = context { + Some(KeymapContextPredicate::parse(context)?) + } else { + None + }; + + let keystrokes = keystrokes + .split_whitespace() + .map(Keystroke::parse) + .collect::>()?; + + Ok(Self { + keystrokes, + action, + context_predicate: context, + }) + } + + pub fn matches_context(&self, contexts: &[KeymapContext]) -> bool { + self.context_predicate + .as_ref() + .map(|predicate| predicate.eval(contexts)) + .unwrap_or(true) + } + + pub fn match_keystrokes( + &self, + pending_keystrokes: &[Keystroke], + contexts: &[KeymapContext], + ) -> KeyMatch { + if self.keystrokes.as_ref().starts_with(&pending_keystrokes) + && self.matches_context(contexts) + { + // If the binding is completed, push it onto the matches list + if self.keystrokes.as_ref().len() == pending_keystrokes.len() { + KeyMatch::Some(self.action.boxed_clone()) + } else { + KeyMatch::Pending + } + } else { + KeyMatch::None + } + } + + pub fn keystrokes_for_action( + &self, + action: &dyn Action, + contexts: &[KeymapContext], + ) -> Option> { + if self.action.partial_eq(action) && self.matches_context(contexts) { + Some(self.keystrokes.clone()) + } else { + None + } + } + + pub fn keystrokes(&self) -> &[Keystroke] { + self.keystrokes.as_slice() + } + + pub fn action(&self) -> &dyn Action { + self.action.as_ref() + } +} diff --git a/crates/gpui3/src/keymap_matcher/keymap.rs b/crates/gpui3/src/keymap_matcher/keymap.rs new file mode 100644 index 0000000000000000000000000000000000000000..f7b537af46e1f27a1702d9cdee7b34bc4059634f --- /dev/null +++ b/crates/gpui3/src/keymap_matcher/keymap.rs @@ -0,0 +1,400 @@ +use crate::{Binding, KeymapContextPredicate, Keystroke}; +use collections::HashSet; +use smallvec::SmallVec; +use std::{any::TypeId, collections::HashMap}; + +#[derive(Copy, Clone, Eq, PartialEq, Default)] +pub struct KeymapVersion(usize); + +#[derive(Default)] +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_id: HashMap>, + disabled_keystrokes: HashMap, HashSet>>, + version: KeymapVersion, +} + +impl Keymap { + pub fn new(bindings: Vec) -> Self { + let mut this = Self::default(); + this.add_bindings(bindings); + this + } + + pub fn version(&self) -> KeymapVersion { + self.version + } + + pub(crate) fn bindings_for_action( + &self, + action_id: TypeId, + ) -> impl Iterator { + self.binding_indices_by_action_id + .get(&action_id) + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|ix| &self.bindings[*ix]) + .filter(|binding| !self.binding_disabled(binding)) + } + + pub fn add_bindings>(&mut self, bindings: T) { + // todo!("no action") + // let no_action_id = (NoAction {}).id(); + let mut new_bindings = Vec::new(); + let has_new_disabled_keystrokes = false; + for binding in bindings { + // if binding.action().id() == no_action_id { + // has_new_disabled_keystrokes |= self + // .disabled_keystrokes + // .entry(binding.keystrokes) + // .or_default() + // .insert(binding.context_predicate); + // } else { + new_bindings.push(binding); + // } + } + + if has_new_disabled_keystrokes { + self.binding_indices_by_action_id.retain(|_, indices| { + indices.retain(|ix| { + let binding = &self.bindings[*ix]; + match self.disabled_keystrokes.get(&binding.keystrokes) { + Some(disabled_predicates) => { + !disabled_predicates.contains(&binding.context_predicate) + } + None => true, + } + }); + !indices.is_empty() + }); + } + + for new_binding in new_bindings { + if !self.binding_disabled(&new_binding) { + self.binding_indices_by_action_id + .entry(new_binding.action().as_any().type_id()) + .or_default() + .push(self.bindings.len()); + self.bindings.push(new_binding); + } + } + + self.version.0 += 1; + } + + pub fn clear(&mut self) { + self.bindings.clear(); + self.binding_indices_by_action_id.clear(); + self.disabled_keystrokes.clear(); + self.version.0 += 1; + } + + pub fn bindings(&self) -> Vec<&Binding> { + self.bindings + .iter() + .filter(|binding| !self.binding_disabled(binding)) + .collect() + } + + fn binding_disabled(&self, binding: &Binding) -> bool { + match self.disabled_keystrokes.get(&binding.keystrokes) { + Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate), + None => false, + } + } +} + +// #[cfg(test)] +// mod tests { +// use crate::actions; + +// use super::*; + +// actions!( +// keymap_test, +// [Present1, Present2, Present3, Duplicate, Missing] +// ); + +// #[test] +// fn regular_keymap() { +// let present_1 = Binding::new("ctrl-q", Present1 {}, None); +// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); +// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); +// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); +// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); +// let missing = Binding::new("ctrl-r", Missing {}, None); +// let all_bindings = [ +// &present_1, +// &present_2, +// &present_3, +// &keystroke_duplicate_to_1, +// &full_duplicate_to_2, +// &missing, +// ]; + +// let mut keymap = Keymap::default(); +// assert_absent(&keymap, &all_bindings); +// assert!(keymap.bindings().is_empty()); + +// keymap.add_bindings([present_1.clone(), present_2.clone(), present_3.clone()]); +// assert_absent(&keymap, &[&keystroke_duplicate_to_1, &missing]); +// assert_present( +// &keymap, +// &[(&present_1, "q"), (&present_2, "w"), (&present_3, "e")], +// ); + +// keymap.add_bindings([ +// keystroke_duplicate_to_1.clone(), +// full_duplicate_to_2.clone(), +// ]); +// assert_absent(&keymap, &[&missing]); +// assert!( +// !keymap.binding_disabled(&keystroke_duplicate_to_1), +// "Duplicate binding 1 was added and should not be disabled" +// ); +// assert!( +// !keymap.binding_disabled(&full_duplicate_to_2), +// "Duplicate binding 2 was added and should not be disabled" +// ); + +// assert_eq!( +// keymap +// .bindings_for_action(keystroke_duplicate_to_1.action().id()) +// .map(|binding| &binding.keystrokes) +// .flatten() +// .collect::>(), +// vec![&Keystroke { +// ctrl: true, +// alt: false, +// shift: false, +// cmd: false, +// function: false, +// key: "q".to_string(), +// ime_key: None, +// }], +// "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap" +// ); +// assert_eq!( +// keymap +// .bindings_for_action(full_duplicate_to_2.action().id()) +// .map(|binding| &binding.keystrokes) +// .flatten() +// .collect::>(), +// vec![ +// &Keystroke { +// ctrl: true, +// alt: false, +// shift: false, +// cmd: false, +// function: false, +// key: "w".to_string(), +// ime_key: None, +// }, +// &Keystroke { +// ctrl: true, +// alt: false, +// shift: false, +// cmd: false, +// function: false, +// key: "w".to_string(), +// ime_key: None, +// } +// ], +// "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap" +// ); + +// let updated_bindings = keymap.bindings(); +// let expected_updated_bindings = vec![ +// &present_1, +// &present_2, +// &present_3, +// &keystroke_duplicate_to_1, +// &full_duplicate_to_2, +// ]; +// assert_eq!( +// updated_bindings.len(), +// expected_updated_bindings.len(), +// "Unexpected updated keymap bindings {updated_bindings:?}" +// ); +// for (i, expected) in expected_updated_bindings.iter().enumerate() { +// let keymap_binding = &updated_bindings[i]; +// assert_eq!( +// keymap_binding.context_predicate, expected.context_predicate, +// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" +// ); +// assert_eq!( +// keymap_binding.keystrokes, expected.keystrokes, +// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" +// ); +// } + +// keymap.clear(); +// assert_absent(&keymap, &all_bindings); +// assert!(keymap.bindings().is_empty()); +// } + +// #[test] +// fn keymap_with_ignored() { +// let present_1 = Binding::new("ctrl-q", Present1 {}, None); +// let present_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); +// let present_3 = Binding::new("ctrl-e", Present3 {}, Some("editor")); +// let keystroke_duplicate_to_1 = Binding::new("ctrl-q", Duplicate {}, None); +// let full_duplicate_to_2 = Binding::new("ctrl-w", Present2 {}, Some("pane")); +// let ignored_1 = Binding::new("ctrl-q", NoAction {}, None); +// let ignored_2 = Binding::new("ctrl-w", NoAction {}, Some("pane")); +// let ignored_3_with_other_context = +// Binding::new("ctrl-e", NoAction {}, Some("other_context")); + +// let mut keymap = Keymap::default(); + +// keymap.add_bindings([ +// ignored_1.clone(), +// ignored_2.clone(), +// ignored_3_with_other_context.clone(), +// ]); +// assert_absent(&keymap, &[&present_3]); +// assert_disabled( +// &keymap, +// &[ +// &present_1, +// &present_2, +// &ignored_1, +// &ignored_2, +// &ignored_3_with_other_context, +// ], +// ); +// assert!(keymap.bindings().is_empty()); +// keymap.clear(); + +// keymap.add_bindings([ +// present_1.clone(), +// present_2.clone(), +// present_3.clone(), +// ignored_1.clone(), +// ignored_2.clone(), +// ignored_3_with_other_context.clone(), +// ]); +// assert_present(&keymap, &[(&present_3, "e")]); +// assert_disabled( +// &keymap, +// &[ +// &present_1, +// &present_2, +// &ignored_1, +// &ignored_2, +// &ignored_3_with_other_context, +// ], +// ); +// keymap.clear(); + +// keymap.add_bindings([ +// present_1.clone(), +// present_2.clone(), +// present_3.clone(), +// ignored_1.clone(), +// ]); +// assert_present(&keymap, &[(&present_2, "w"), (&present_3, "e")]); +// assert_disabled(&keymap, &[&present_1, &ignored_1]); +// assert_absent(&keymap, &[&ignored_2, &ignored_3_with_other_context]); +// keymap.clear(); + +// keymap.add_bindings([ +// present_1.clone(), +// present_2.clone(), +// present_3.clone(), +// keystroke_duplicate_to_1.clone(), +// full_duplicate_to_2.clone(), +// ignored_1.clone(), +// ignored_2.clone(), +// ignored_3_with_other_context.clone(), +// ]); +// assert_present(&keymap, &[(&present_3, "e")]); +// assert_disabled( +// &keymap, +// &[ +// &present_1, +// &present_2, +// &keystroke_duplicate_to_1, +// &full_duplicate_to_2, +// &ignored_1, +// &ignored_2, +// &ignored_3_with_other_context, +// ], +// ); +// keymap.clear(); +// } + +// #[track_caller] +// fn assert_present(keymap: &Keymap, expected_bindings: &[(&Binding, &str)]) { +// let keymap_bindings = keymap.bindings(); +// assert_eq!( +// expected_bindings.len(), +// keymap_bindings.len(), +// "Unexpected keymap bindings {keymap_bindings:?}" +// ); +// for (i, (expected, expected_key)) in expected_bindings.iter().enumerate() { +// assert!( +// !keymap.binding_disabled(expected), +// "{expected:?} should not be disabled as it was added into keymap for element {i}" +// ); +// assert_eq!( +// keymap +// .bindings_for_action(expected.action().id()) +// .map(|binding| &binding.keystrokes) +// .flatten() +// .collect::>(), +// vec![&Keystroke { +// ctrl: true, +// alt: false, +// shift: false, +// cmd: false, +// function: false, +// key: expected_key.to_string(), +// ime_key: None, +// }], +// "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}" +// ); + +// let keymap_binding = &keymap_bindings[i]; +// assert_eq!( +// keymap_binding.context_predicate, expected.context_predicate, +// "Unexpected context predicate for keymap {i} element: {keymap_binding:?}" +// ); +// assert_eq!( +// keymap_binding.keystrokes, expected.keystrokes, +// "Unexpected keystrokes for keymap {i} element: {keymap_binding:?}" +// ); +// } +// } + +// #[track_caller] +// fn assert_absent(keymap: &Keymap, bindings: &[&Binding]) { +// for binding in bindings.iter() { +// assert!( +// !keymap.binding_disabled(binding), +// "{binding:?} should not be disabled in the keymap where was not added" +// ); +// assert_eq!( +// keymap.bindings_for_action(binding.action().id()).count(), +// 0, +// "{binding:?} should have no actions in the keymap where was not added" +// ); +// } +// } + +// #[track_caller] +// fn assert_disabled(keymap: &Keymap, bindings: &[&Binding]) { +// for binding in bindings.iter() { +// assert!( +// keymap.binding_disabled(binding), +// "{binding:?} should be disabled in the keymap" +// ); +// assert_eq!( +// keymap.bindings_for_action(binding.action().id()).count(), +// 0, +// "{binding:?} should have no actions in the keymap where it was disabled" +// ); +// } +// } +// } diff --git a/crates/gpui3/src/keymap_matcher/keymap_context.rs b/crates/gpui3/src/keymap_matcher/keymap_context.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ceac72e8f923a3fab5e434f659a4183db9dd8b0 --- /dev/null +++ b/crates/gpui3/src/keymap_matcher/keymap_context.rs @@ -0,0 +1,325 @@ +use anyhow::{anyhow, Result}; +use collections::{HashMap, HashSet}; +use std::borrow::Cow; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct KeymapContext { + set: HashSet>, + map: HashMap, Cow<'static, str>>, +} + +impl KeymapContext { + pub fn new() -> Self { + KeymapContext { + set: HashSet::default(), + map: HashMap::default(), + } + } + + pub fn clear(&mut self) { + self.set.clear(); + self.map.clear(); + } + + pub fn extend(&mut self, other: &Self) { + for v in &other.set { + self.set.insert(v.clone()); + } + for (k, v) in &other.map { + self.map.insert(k.clone(), v.clone()); + } + } + + pub fn add_identifier>>(&mut self, identifier: I) { + self.set.insert(identifier.into()); + } + + pub fn add_key>, S2: Into>>( + &mut self, + key: S1, + value: S2, + ) { + self.map.insert(key.into(), value.into()); + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeymapContextPredicate { + Identifier(String), + Equal(String, String), + NotEqual(String, String), + Child(Box, Box), + Not(Box), + And(Box, Box), + Or(Box, Box), +} + +impl KeymapContextPredicate { + pub fn parse(source: &str) -> Result { + let source = Self::skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + Err(anyhow!("unexpected character {next:?}")) + } else { + Ok(predicate) + } + } + + pub fn eval(&self, contexts: &[KeymapContext]) -> bool { + let Some(context) = contexts.first() else { + return false; + }; + match self { + Self::Identifier(name) => (&context.set).contains(name.as_str()), + Self::Equal(left, right) => context + .map + .get(left.as_str()) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .map + .get(left.as_str()) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => !pred.eval(contexts), + Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts), + Self::And(left, right) => left.eval(contexts) && right.eval(contexts), + Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = + fn(KeymapContextPredicate, KeymapContextPredicate) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = Self::skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source + .chars() + .next() + .ok_or_else(|| anyhow!("unexpected eof"))?; + match next { + '(' => { + source = Self::skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if rest.starts_with(')') { + source = Self::skip_whitespace(&rest[1..]); + Ok((predicate, source)) + } else { + Err(anyhow!("expected a ')'")) + } + } + '!' => { + let source = Self::skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; + Ok((KeymapContextPredicate::Not(Box::new(predicate)), source)) + } + _ if next.is_alphanumeric() || next == '_' => { + let len = source + .find(|c: char| !(c.is_alphanumeric() || c == '_')) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = Self::skip_whitespace(rest); + Ok(( + KeymapContextPredicate::Identifier(identifier.into()), + source, + )) + } + _ => Err(anyhow!("unexpected character {next:?}")), + } + } + + fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +#[cfg(test)] +mod tests { + use super::KeymapContextPredicate::{self, *}; + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeymapContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeymapContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeymapContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeymapContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeymapContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeymapContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeymapContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeymapContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeymapContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeymapContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeymapContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeymapContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } +} diff --git a/crates/gpui3/src/platform/keystroke.rs b/crates/gpui3/src/platform/keystroke.rs index d406083f1d27209a084fcf20b6f2715644408a9b..cadb9c92e3f4dc53d23671dfb630a8560bf55c7e 100644 --- a/crates/gpui3/src/platform/keystroke.rs +++ b/crates/gpui3/src/platform/keystroke.rs @@ -1,29 +1,54 @@ use anyhow::anyhow; use serde::Deserialize; +use smallvec::SmallVec; use std::fmt::Write; -#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Deserialize, Hash)] -pub struct Modifiers { - pub control: bool, - pub alt: bool, - pub shift: bool, - pub command: bool, - pub function: bool, -} - -impl Modifiers { - pub fn modified(&self) -> bool { - self.control || self.alt || self.shift || self.command || self.function - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Hash)] +#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { - pub key: String, pub modifiers: Modifiers, + /// key is the character printed on the key that was pressed + /// e.g. for option-s, key is "s" + pub key: String, + /// ime_key is the character inserted by the IME engine when that key was pressed. + /// e.g. for option-s, ime_key is "ß" + pub ime_key: Option, } impl Keystroke { + // When matching a key we cannot know whether the user intended to type + // the ime_key or the key. On some non-US keyboards keys we use in our + // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + // and on some keyboards the IME handler converts a sequence of keys into a + // specific character (for example `"` is typed as `" space` on a brazillian keyboard). + pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { + let mut possibilities = SmallVec::new(); + match self.ime_key.as_ref() { + None => possibilities.push(self.clone()), + Some(ime_key) => { + possibilities.push(Keystroke { + modifiers: Modifiers { + control: self.modifiers.control, + alt: false, + shift: false, + command: false, + function: false, + }, + key: ime_key.to_string(), + ime_key: None, + }); + possibilities.push(Keystroke { + ime_key: None, + ..self.clone() + }); + } + } + possibilities + } + + /// key syntax is: + /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] + /// ime_key is only used for generating test events, + /// when matching a key with an ime_key set will be matched without it. pub fn parse(source: &str) -> anyhow::Result { let mut control = false; let mut alt = false; @@ -31,6 +56,7 @@ impl Keystroke { let mut command = false; let mut function = false; let mut key = None; + let mut ime_key = None; let mut components = source.split('-').peekable(); while let Some(component) = components.next() { @@ -41,10 +67,14 @@ impl Keystroke { "cmd" => command = true, "fn" => function = true, _ => { - if let Some(component) = components.peek() { - if component.is_empty() && source.ends_with('-') { + if let Some(next) = components.peek() { + if next.is_empty() && source.ends_with('-') { key = Some(String::from("-")); break; + } else if next.len() > 1 && next.starts_with('>') { + key = Some(String::from(component)); + ime_key = Some(String::from(&next[1..])); + components.next(); } else { return Err(anyhow!("Invalid keystroke `{}`", source)); } @@ -66,40 +96,25 @@ impl Keystroke { function, }, key, + ime_key, }) } - - pub fn modified(&self) -> bool { - self.modifiers.modified() - } } impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Modifiers { - control, - alt, - shift, - command, - function, - } = self.modifiers; - - if control { + if self.modifiers.control { f.write_char('^')?; } - if alt { - f.write_char('⎇')?; + if self.modifiers.alt { + f.write_char('⌥')?; } - if command { + if self.modifiers.command { f.write_char('⌘')?; } - if shift { + if self.modifiers.shift { f.write_char('⇧')?; } - if function { - f.write_char('𝙛')?; - } - let key = match self.key.as_str() { "backspace" => '⌫', "up" => '↑', @@ -119,3 +134,18 @@ impl std::fmt::Display for Keystroke { f.write_char(key) } } + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] +pub struct Modifiers { + pub control: bool, + pub alt: bool, + pub shift: bool, + pub command: bool, + pub function: bool, +} + +impl Modifiers { + pub fn modified(&self) -> bool { + self.control || self.alt || self.shift || self.command || self.function + } +} diff --git a/crates/gpui3/src/platform/mac/events.rs b/crates/gpui3/src/platform/mac/events.rs index fa8277baf477299d9a9eb6b991c28a5f03ca7ceb..916b9a18bebc3b1c4e07a98a9c045efbb2917c9b 100644 --- a/crates/gpui3/src/platform/mac/events.rs +++ b/crates/gpui3/src/platform/mac/events.rs @@ -325,6 +325,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { function, }, key, + ime_key: None, } } diff --git a/crates/gpui3/src/platform/mac/window.rs b/crates/gpui3/src/platform/mac/window.rs index 519eab5b093a1a0e9fbaf6b9446522df689b1f12..f72c6ad41629b4c751f111a2d2236953ca251e84 100644 --- a/crates/gpui3/src/platform/mac/window.rs +++ b/crates/gpui3/src/platform/mac/window.rs @@ -1043,6 +1043,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: // we don't match cmd/fn because they don't seem to use IME modifiers: Default::default(), key: ime_text.clone().unwrap(), + ime_key: None, // todo!("handle IME key") }, }; handled = callback(InputEvent::KeyDown(event_with_ime_text)); @@ -1203,6 +1204,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let keystroke = Keystroke { modifiers: Default::default(), key: ".".into(), + ime_key: None, }; let event = InputEvent::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), diff --git a/crates/gpui3/src/text_system.rs b/crates/gpui3/src/text_system.rs index 5315e4e3573a96e420627f7d3f6435ecb986a404..3b16bb506c1f89e7316f4918dcc3052021deda25 100644 --- a/crates/gpui3/src/text_system.rs +++ b/crates/gpui3/src/text_system.rs @@ -219,8 +219,8 @@ impl TextSystem { Ok(lines) } - pub fn end_frame(&self) { - self.line_layout_cache.end_frame() + pub fn start_frame(&self) { + self.line_layout_cache.start_frame() } pub fn line_wrapper( diff --git a/crates/gpui3/src/text_system/line_layout.rs b/crates/gpui3/src/text_system/line_layout.rs index 5d83bd69f6a1654638afa5baa012793ad34cf6c9..8ce859218b35241066f8917d188aaf7c7c279600 100644 --- a/crates/gpui3/src/text_system/line_layout.rs +++ b/crates/gpui3/src/text_system/line_layout.rs @@ -167,7 +167,7 @@ impl LineLayoutCache { } } - pub fn end_frame(&self) { + pub fn start_frame(&self) { let mut prev_frame = self.prev_frame.lock(); let mut curr_frame = self.curr_frame.write(); std::mem::swap(&mut *prev_frame, &mut *curr_frame); diff --git a/crates/gpui3/src/view.rs b/crates/gpui3/src/view.rs index cc9d2d3d27824c18ac1d2011e821a4b85724802b..e299e2c5de5218781ad179d1b731230d880d1f4b 100644 --- a/crates/gpui3/src/view.rs +++ b/crates/gpui3/src/view.rs @@ -164,7 +164,7 @@ impl ViewObject for View { } fn initialize(&mut self, cx: &mut WindowContext) -> AnyBox { - cx.with_element_id(self.entity_id(), |cx| { + cx.with_element_id(self.entity_id(), |_global_id, cx| { self.state.update(cx, |state, cx| { let mut any_element = Box::new((self.render)(state, cx)); any_element.initialize(state, cx); @@ -174,7 +174,7 @@ impl ViewObject for View { } fn layout(&mut self, element: &mut AnyBox, cx: &mut WindowContext) -> LayoutId { - cx.with_element_id(self.entity_id(), |cx| { + cx.with_element_id(self.entity_id(), |_global_id, cx| { self.state.update(cx, |state, cx| { let element = element.downcast_mut::>().unwrap(); element.layout(state, cx) @@ -183,7 +183,7 @@ impl ViewObject for View { } fn paint(&mut self, _: Bounds, element: &mut AnyBox, cx: &mut WindowContext) { - cx.with_element_id(self.entity_id(), |cx| { + cx.with_element_id(self.entity_id(), |_global_id, cx| { self.state.update(cx, |state, cx| { let element = element.downcast_mut::>().unwrap(); element.paint(state, None, cx); diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index 545c07ed2f8997cb477cd7874ac93f6207aca62c..5ab1902bbfc8cd4ea83e89d0067a46bade3c2779 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -1,12 +1,13 @@ use crate::{ - px, size, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext, - Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId, - EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, ImageData, - InputEvent, IsZero, KeyListener, KeyMatch, Keystroke, LayoutId, MainThread, MainThreadOnly, - MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, - PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, - ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, - Task, Underline, UnderlineStyle, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS, + px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, + BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, + Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, + ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeymapMatcher, Keystroke, LayoutId, + MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform, + PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, + RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, + Style, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, WeakHandle, + WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::Result; use collections::HashMap; @@ -46,7 +47,10 @@ pub enum DispatchPhase { type AnyListener = Arc; type AnyKeyListener = Arc< - dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) -> Option + Send + Sync + 'static, + dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) -> Option> + + Send + + Sync + + 'static, >; type AnyFocusListener = Arc; @@ -144,8 +148,10 @@ pub struct Window { layout_engine: TaffyLayoutEngine, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, - prev_element_states: HashMap, + prev_frame_element_states: HashMap, element_states: HashMap, + prev_frame_key_matchers: HashMap, + key_matchers: HashMap, z_index_stack: StackingOrder, content_mask_stack: Vec>, mouse_listeners: HashMap>, @@ -217,8 +223,10 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - prev_element_states: HashMap::default(), + prev_frame_element_states: HashMap::default(), element_states: HashMap::default(), + prev_frame_key_matchers: HashMap::default(), + key_matchers: HashMap::default(), z_index_stack: StackingOrder(SmallVec::new()), content_mask_stack: Vec::new(), mouse_listeners: HashMap::default(), @@ -767,7 +775,6 @@ impl<'a, 'w> WindowContext<'a, 'w> { cx.window.root_view = Some(root_view); let scene = cx.window.scene_builder.build(); - cx.end_frame(); cx.run_on_main(view, |_, cx| { cx.window @@ -797,13 +804,28 @@ impl<'a, 'w> WindowContext<'a, 'w> { } fn start_frame(&mut self) { - // Make the current element states the previous, and then clear the current. - // The empty element states map will be populated for any element states we - // reference during the upcoming frame. + self.text_system().start_frame(); + let window = &mut *self.window; - mem::swap(&mut window.element_states, &mut window.prev_element_states); + + // Move the current frame element states to the previous frame. + // The new empty element states map will be populated for any element states we + // reference during the upcoming frame. + mem::swap( + &mut window.element_states, + &mut window.prev_frame_element_states, + ); window.element_states.clear(); + // Make the current key matchers the previous, and then clear the current. + // An empty key matcher map will be created for every identified element in the + // upcoming frame. + mem::swap( + &mut window.key_matchers, + &mut window.prev_frame_key_matchers, + ); + window.key_matchers.clear(); + // Clear mouse event listeners, because elements add new element listeners // when the upcoming frame is painted. window.mouse_listeners.values_mut().for_each(Vec::clear); @@ -816,10 +838,6 @@ impl<'a, 'w> WindowContext<'a, 'w> { window.key_events_enabled = true; } - fn end_frame(&mut self) { - self.text_system().end_frame(); - } - fn dispatch_event(&mut self, event: InputEvent) -> bool { if let Some(any_mouse_event) = event.mouse_event() { if let Some(MouseMoveEvent { position, .. }) = any_mouse_event.downcast_ref() { @@ -905,15 +923,32 @@ impl<'a, 'w> WindowContext<'a, 'w> { true } - pub fn match_keystroke(&mut self, element_id: &ElementId, keystroke: &Keystroke) -> KeyMatch { - todo!(); + pub fn match_keystroke( + &mut self, + element_id: &GlobalElementId, + keystroke: &Keystroke, + ) -> KeyMatch { + let key_match = self + .window + .key_matchers + .get_mut(element_id) + .unwrap() + .match_keystroke(keystroke, &[]); + + if key_match.is_some() { + for matcher in self.window.key_matchers.values_mut() { + matcher.clear_pending(); + } + } + + key_match } - fn dispatch_action(&mut self, action: AnyBox, listeners: &[(TypeId, AnyKeyListener)]) { + fn dispatch_action(&mut self, action: Box, listeners: &[(TypeId, AnyKeyListener)]) { let action_type = action.type_id(); for (event_type, listener) in listeners { if action_type == *event_type { - listener(action.as_ref(), DispatchPhase::Capture, self); + listener(action.as_any(), DispatchPhase::Capture, self); if !self.window.propagate { break; } @@ -923,7 +958,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { if self.window.propagate { for (event_type, listener) in listeners.iter().rev() { if action_type == *event_type { - listener(action.as_ref(), DispatchPhase::Capture, self); + listener(action.as_any(), DispatchPhase::Capture, self); if !self.window.propagate { break; } @@ -998,10 +1033,11 @@ pub trait BorrowWindow: BorrowAppContext { fn with_element_id( &mut self, id: impl Into, - f: impl FnOnce(&mut Self) -> R, + f: impl FnOnce(GlobalElementId, &mut Self) -> R, ) -> R { self.window_mut().element_id_stack.push(id.into()); - let result = f(self); + let global_id = self.window_mut().element_id_stack.clone(); + let result = f(global_id, self); self.window_mut().element_id_stack.pop(); result } @@ -1023,13 +1059,12 @@ pub trait BorrowWindow: BorrowAppContext { id: ElementId, f: impl FnOnce(Option, &mut Self) -> (R, S), ) -> R { - self.with_element_id(id, |cx| { - let global_id = cx.window_mut().element_id_stack.clone(); + self.with_element_id(id, |global_id, cx| { if let Some(any) = cx .window_mut() .element_states .remove(&global_id) - .or_else(|| cx.window_mut().prev_element_states.remove(&global_id)) + .or_else(|| cx.window_mut().prev_frame_element_states.remove(&global_id)) { // Using the extra inner option to avoid needing to reallocate a new box. let mut state_box = any From 7fef03a7db224acd55391a285bb92c4f293c28cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:09:11 +0200 Subject: [PATCH 10/20] Checkpoint --- crates/gpui3/src/gpui3.rs | 4 ++-- .../src/{keymap_matcher => keymap}/binding.rs | 0 .../src/{keymap_matcher => keymap}/keymap.rs | 0 .../{keymap_matcher => keymap}/keymap_context.rs | 0 .../src/{keymap_matcher.rs => keymap/matcher.rs} | 16 ++++------------ crates/gpui3/src/keymap/mod.rs | 9 +++++++++ crates/gpui3/src/window.rs | 6 +++--- 7 files changed, 18 insertions(+), 17 deletions(-) rename crates/gpui3/src/{keymap_matcher => keymap}/binding.rs (100%) rename crates/gpui3/src/{keymap_matcher => keymap}/keymap.rs (100%) rename crates/gpui3/src/{keymap_matcher => keymap}/keymap_context.rs (100%) rename crates/gpui3/src/{keymap_matcher.rs => keymap/matcher.rs} (98%) create mode 100644 crates/gpui3/src/keymap/mod.rs diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index cdefb94edd1bd4386792013741e9d85a8f03a36a..1af9a1cc222386f812fbf1e9c6d535bfafc968e8 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -11,7 +11,7 @@ mod geometry; mod hover; mod image_cache; mod interactive; -mod keymap_matcher; +mod keymap; mod platform; mod scene; mod style; @@ -39,7 +39,7 @@ pub use gpui3_macros::*; pub use hover::*; pub use image_cache::*; pub use interactive::*; -pub use keymap_matcher::*; +pub use keymap::*; pub use platform::*; pub use refineable::*; pub use scene::*; diff --git a/crates/gpui3/src/keymap_matcher/binding.rs b/crates/gpui3/src/keymap/binding.rs similarity index 100% rename from crates/gpui3/src/keymap_matcher/binding.rs rename to crates/gpui3/src/keymap/binding.rs diff --git a/crates/gpui3/src/keymap_matcher/keymap.rs b/crates/gpui3/src/keymap/keymap.rs similarity index 100% rename from crates/gpui3/src/keymap_matcher/keymap.rs rename to crates/gpui3/src/keymap/keymap.rs diff --git a/crates/gpui3/src/keymap_matcher/keymap_context.rs b/crates/gpui3/src/keymap/keymap_context.rs similarity index 100% rename from crates/gpui3/src/keymap_matcher/keymap_context.rs rename to crates/gpui3/src/keymap/keymap_context.rs diff --git a/crates/gpui3/src/keymap_matcher.rs b/crates/gpui3/src/keymap/matcher.rs similarity index 98% rename from crates/gpui3/src/keymap_matcher.rs rename to crates/gpui3/src/keymap/matcher.rs index 955b1890936a1ce74f5c054fd4d481cbddd1524b..38c8cc42f0cc46e40d25dfb32a2f12b1cc57d3a0 100644 --- a/crates/gpui3/src/keymap_matcher.rs +++ b/crates/gpui3/src/keymap/matcher.rs @@ -1,12 +1,4 @@ -mod binding; -mod keymap; -mod keymap_context; - -pub use binding::*; -pub use keymap::*; -pub use keymap_context::*; - -use crate::Keystroke; +use crate::{Binding, Keymap, KeymapContext, KeymapVersion, Keystroke}; use smallvec::SmallVec; use std::any::{Any, TypeId}; @@ -16,13 +8,13 @@ pub trait Action: Any + Send + Sync { fn as_any(&self) -> &dyn Any; } -pub struct KeymapMatcher { +pub struct KeyMatcher { pending_keystrokes: Vec, keymap: Keymap, keymap_version: KeymapVersion, } -impl KeymapMatcher { +impl KeyMatcher { pub fn new(keymap: Keymap) -> Self { Self { pending_keystrokes: Vec::new(), @@ -106,7 +98,7 @@ impl KeymapMatcher { } } -impl Default for KeymapMatcher { +impl Default for KeyMatcher { fn default() -> Self { Self::new(Keymap::default()) } diff --git a/crates/gpui3/src/keymap/mod.rs b/crates/gpui3/src/keymap/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..26fcfac477fb73a70f6151ea4c9d2ea155d88d95 --- /dev/null +++ b/crates/gpui3/src/keymap/mod.rs @@ -0,0 +1,9 @@ +mod binding; +mod keymap; +mod keymap_context; +mod matcher; + +pub use binding::*; +pub use keymap::*; +pub use keymap_context::*; +pub use matcher::*; diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index 5ab1902bbfc8cd4ea83e89d0067a46bade3c2779..02160d68329e8687cd9fadb447f906e20e1e81b5 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -2,7 +2,7 @@ use crate::{ px, size, Action, AnyBox, AnyView, AppContext, AsyncWindowContext, AvailableSpace, BorrowAppContext, Bounds, BoxShadow, Context, Corners, DevicePixels, DisplayId, Edges, Effect, Element, EntityId, EventEmitter, FocusEvent, FontId, GlobalElementId, GlyphId, Handle, Hsla, - ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeymapMatcher, Keystroke, LayoutId, + ImageData, InputEvent, IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, MainThread, MainThreadOnly, MonochromeSprite, MouseMoveEvent, Path, Pixels, Platform, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Quad, Reference, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, @@ -150,8 +150,8 @@ pub struct Window { pub(crate) element_id_stack: GlobalElementId, prev_frame_element_states: HashMap, element_states: HashMap, - prev_frame_key_matchers: HashMap, - key_matchers: HashMap, + prev_frame_key_matchers: HashMap, + key_matchers: HashMap, z_index_stack: StackingOrder, content_mask_stack: Vec>, mouse_listeners: HashMap>, From 93ff79febf8d66d5b867a7feb5800d4406a76442 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:12:35 +0200 Subject: [PATCH 11/20] Checkpoint --- crates/gpui3/src/keymap/keymap.rs | 5 +---- crates/gpui3/src/keymap/matcher.rs | 35 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/gpui3/src/keymap/keymap.rs b/crates/gpui3/src/keymap/keymap.rs index f7b537af46e1f27a1702d9cdee7b34bc4059634f..6bbb87a0e3ae9865644e4e058f8c116fdad10cf3 100644 --- a/crates/gpui3/src/keymap/keymap.rs +++ b/crates/gpui3/src/keymap/keymap.rs @@ -25,10 +25,7 @@ impl Keymap { self.version } - pub(crate) fn bindings_for_action( - &self, - action_id: TypeId, - ) -> impl Iterator { + pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { self.binding_indices_by_action_id .get(&action_id) .map(SmallVec::as_slice) diff --git a/crates/gpui3/src/keymap/matcher.rs b/crates/gpui3/src/keymap/matcher.rs index 38c8cc42f0cc46e40d25dfb32a2f12b1cc57d3a0..38d2f38029726561dabede829957b3c78fde99fa 100644 --- a/crates/gpui3/src/keymap/matcher.rs +++ b/crates/gpui3/src/keymap/matcher.rs @@ -1,6 +1,7 @@ -use crate::{Binding, Keymap, KeymapContext, KeymapVersion, Keystroke}; +use crate::{Keymap, KeymapContext, KeymapVersion, Keystroke}; +use parking_lot::RwLock; use smallvec::SmallVec; -use std::any::{Any, TypeId}; +use std::{any::Any, sync::Arc}; pub trait Action: Any + Send + Sync { fn partial_eq(&self, action: &dyn Action) -> bool; @@ -10,22 +11,24 @@ pub trait Action: Any + Send + Sync { pub struct KeyMatcher { pending_keystrokes: Vec, - keymap: Keymap, + keymap: Arc>, keymap_version: KeymapVersion, } impl KeyMatcher { - pub fn new(keymap: Keymap) -> Self { + pub fn new(keymap: Arc>) -> Self { + let keymap_version = keymap.read().version(); Self { pending_keystrokes: Vec::new(), - keymap_version: keymap.version(), + keymap_version, keymap, } } - pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { - self.keymap.bindings_for_action(action_id) - } + // todo!("replace with a function that calls an FnMut for every binding matching the action") + // pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { + // self.keymap.read().bindings_for_action(action_id) + // } pub fn clear_pending(&mut self) { self.pending_keystrokes.clear(); @@ -49,20 +52,21 @@ impl KeyMatcher { keystroke: &Keystroke, context_stack: &[KeymapContext], ) -> KeyMatch { + let keymap = self.keymap.read(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. - if self.keymap.version() != self.keymap_version { - self.keymap_version = self.keymap.version(); + if keymap.version() != self.keymap_version { + self.keymap_version = keymap.version(); self.pending_keystrokes.clear(); } let mut pending_key = None; - for binding in self.keymap.bindings().iter().rev() { + for binding in keymap.bindings().iter().rev() { for candidate in keystroke.match_candidates() { self.pending_keystrokes.push(candidate.clone()); match binding.match_keystrokes(&self.pending_keystrokes, context_stack) { KeyMatch::Some(action) => { - self.clear_pending(); + self.pending_keystrokes.clear(); return KeyMatch::Some(action); } KeyMatch::Pending => { @@ -91,6 +95,7 @@ impl KeyMatcher { contexts: &[KeymapContext], ) -> Option> { self.keymap + .read() .bindings() .iter() .rev() @@ -98,12 +103,6 @@ impl KeyMatcher { } } -impl Default for KeyMatcher { - fn default() -> Self { - Self::new(Keymap::default()) - } -} - pub enum KeyMatch { None, Pending, From 90d34c1251747e34901681e19414e101f62a4a05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:26:52 +0200 Subject: [PATCH 12/20] Checkpoint --- .../{keymap/keymap_context.rs => action.rs} | 89 ++++++++++--------- crates/gpui3/src/app.rs | 18 +++- crates/gpui3/src/gpui3.rs | 2 + crates/gpui3/src/keymap/binding.rs | 16 ++-- crates/gpui3/src/keymap/keymap.rs | 16 ++-- crates/gpui3/src/keymap/matcher.rs | 44 ++++----- crates/gpui3/src/keymap/mod.rs | 2 - 7 files changed, 97 insertions(+), 90 deletions(-) rename crates/gpui3/src/{keymap/keymap_context.rs => action.rs} (78%) diff --git a/crates/gpui3/src/keymap/keymap_context.rs b/crates/gpui3/src/action.rs similarity index 78% rename from crates/gpui3/src/keymap/keymap_context.rs rename to crates/gpui3/src/action.rs index 8ceac72e8f923a3fab5e434f659a4183db9dd8b0..e8d113532dc59d9a24eda50f725af800065bdd92 100644 --- a/crates/gpui3/src/keymap/keymap_context.rs +++ b/crates/gpui3/src/action.rs @@ -1,16 +1,23 @@ +use crate::SharedString; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet}; -use std::borrow::Cow; +use std::any::Any; + +pub trait Action: Any + Send + Sync { + fn partial_eq(&self, action: &dyn Action) -> bool; + fn boxed_clone(&self) -> Box; + fn as_any(&self) -> &dyn Any; +} #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct KeymapContext { - set: HashSet>, - map: HashMap, Cow<'static, str>>, +pub struct ActionContext { + set: HashSet, + map: HashMap, } -impl KeymapContext { +impl ActionContext { pub fn new() -> Self { - KeymapContext { + ActionContext { set: HashSet::default(), map: HashMap::default(), } @@ -30,31 +37,27 @@ impl KeymapContext { } } - pub fn add_identifier>>(&mut self, identifier: I) { + pub fn add_identifier>(&mut self, identifier: I) { self.set.insert(identifier.into()); } - pub fn add_key>, S2: Into>>( - &mut self, - key: S1, - value: S2, - ) { + pub fn add_key, S2: Into>(&mut self, key: S1, value: S2) { self.map.insert(key.into(), value.into()); } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum KeymapContextPredicate { - Identifier(String), - Equal(String, String), - NotEqual(String, String), - Child(Box, Box), - Not(Box), - And(Box, Box), - Or(Box, Box), +pub enum ActionContextPredicate { + Identifier(SharedString), + Equal(SharedString, SharedString), + NotEqual(SharedString, SharedString), + Child(Box, Box), + Not(Box), + And(Box, Box), + Or(Box, Box), } -impl KeymapContextPredicate { +impl ActionContextPredicate { pub fn parse(source: &str) -> Result { let source = Self::skip_whitespace(source); let (predicate, rest) = Self::parse_expr(source, 0)?; @@ -65,20 +68,20 @@ impl KeymapContextPredicate { } } - pub fn eval(&self, contexts: &[KeymapContext]) -> bool { + pub fn eval(&self, contexts: &[ActionContext]) -> bool { let Some(context) = contexts.first() else { return false; }; match self { - Self::Identifier(name) => (&context.set).contains(name.as_str()), + Self::Identifier(name) => context.set.contains(&name), Self::Equal(left, right) => context .map - .get(left.as_str()) + .get(&left) .map(|value| value == right) .unwrap_or(false), Self::NotEqual(left, right) => context .map - .get(left.as_str()) + .get(&left) .map(|value| value != right) .unwrap_or(true), Self::Not(pred) => !pred.eval(contexts), @@ -90,7 +93,7 @@ impl KeymapContextPredicate { fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { type Op = - fn(KeymapContextPredicate, KeymapContextPredicate) -> Result; + fn(ActionContextPredicate, ActionContextPredicate) -> Result; let (mut predicate, rest) = Self::parse_primary(source)?; source = rest; @@ -136,7 +139,7 @@ impl KeymapContextPredicate { '!' => { let source = Self::skip_whitespace(&source[1..]); let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; - Ok((KeymapContextPredicate::Not(Box::new(predicate)), source)) + Ok((ActionContextPredicate::Not(Box::new(predicate)), source)) } _ if next.is_alphanumeric() || next == '_' => { let len = source @@ -145,7 +148,7 @@ impl KeymapContextPredicate { let (identifier, rest) = source.split_at(len); source = Self::skip_whitespace(rest); Ok(( - KeymapContextPredicate::Identifier(identifier.into()), + ActionContextPredicate::Identifier(identifier.to_string().into()), source, )) } @@ -197,17 +200,17 @@ const PRECEDENCE_NOT: u32 = 5; #[cfg(test)] mod tests { - use super::KeymapContextPredicate::{self, *}; + use super::ActionContextPredicate::{self, *}; #[test] fn test_parse_identifiers() { // Identifiers assert_eq!( - KeymapContextPredicate::parse("abc12").unwrap(), + ActionContextPredicate::parse("abc12").unwrap(), Identifier("abc12".into()) ); assert_eq!( - KeymapContextPredicate::parse("_1a").unwrap(), + ActionContextPredicate::parse("_1a").unwrap(), Identifier("_1a".into()) ); } @@ -215,11 +218,11 @@ mod tests { #[test] fn test_parse_negations() { assert_eq!( - KeymapContextPredicate::parse("!abc").unwrap(), + ActionContextPredicate::parse("!abc").unwrap(), Not(Box::new(Identifier("abc".into()))) ); assert_eq!( - KeymapContextPredicate::parse(" ! ! abc").unwrap(), + ActionContextPredicate::parse(" ! ! abc").unwrap(), Not(Box::new(Not(Box::new(Identifier("abc".into()))))) ); } @@ -227,15 +230,15 @@ mod tests { #[test] fn test_parse_equality_operators() { assert_eq!( - KeymapContextPredicate::parse("a == b").unwrap(), + ActionContextPredicate::parse("a == b").unwrap(), Equal("a".into(), "b".into()) ); assert_eq!( - KeymapContextPredicate::parse("c!=d").unwrap(), + ActionContextPredicate::parse("c!=d").unwrap(), NotEqual("c".into(), "d".into()) ); assert_eq!( - KeymapContextPredicate::parse("c == !d") + ActionContextPredicate::parse("c == !d") .unwrap_err() .to_string(), "operands must be identifiers" @@ -245,14 +248,14 @@ mod tests { #[test] fn test_parse_boolean_operators() { assert_eq!( - KeymapContextPredicate::parse("a || b").unwrap(), + ActionContextPredicate::parse("a || b").unwrap(), Or( Box::new(Identifier("a".into())), Box::new(Identifier("b".into())) ) ); assert_eq!( - KeymapContextPredicate::parse("a || !b && c").unwrap(), + ActionContextPredicate::parse("a || !b && c").unwrap(), Or( Box::new(Identifier("a".into())), Box::new(And( @@ -262,7 +265,7 @@ mod tests { ) ); assert_eq!( - KeymapContextPredicate::parse("a && b || c&&d").unwrap(), + ActionContextPredicate::parse("a && b || c&&d").unwrap(), Or( Box::new(And( Box::new(Identifier("a".into())), @@ -275,7 +278,7 @@ mod tests { ) ); assert_eq!( - KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(), + ActionContextPredicate::parse("a == b && c || d == e && f").unwrap(), Or( Box::new(And( Box::new(Equal("a".into(), "b".into())), @@ -288,7 +291,7 @@ mod tests { ) ); assert_eq!( - KeymapContextPredicate::parse("a && b && c && d").unwrap(), + ActionContextPredicate::parse("a && b && c && d").unwrap(), And( Box::new(And( Box::new(And( @@ -305,7 +308,7 @@ mod tests { #[test] fn test_parse_parenthesized_expressions() { assert_eq!( - KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(), + ActionContextPredicate::parse("a && (b == c || d != e)").unwrap(), And( Box::new(Identifier("a".into())), Box::new(Or( @@ -315,7 +318,7 @@ mod tests { ), ); assert_eq!( - KeymapContextPredicate::parse(" ( a || b ) ").unwrap(), + ActionContextPredicate::parse(" ( a || b ) ").unwrap(), Or( Box::new(Identifier("a".into())), Box::new(Identifier("b".into())), diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs index f2033514fa8dac8c024f0c324f88cff13e4f63d4..c417bcb0dd8955f9fbb2341b6cc3739288d519f9 100644 --- a/crates/gpui3/src/app.rs +++ b/crates/gpui3/src/app.rs @@ -10,14 +10,14 @@ use smallvec::SmallVec; use crate::{ current_platform, image_cache::ImageCache, AssetSource, Context, DisplayId, Executor, - FocusEvent, FocusHandle, FocusId, LayoutId, MainThread, MainThreadOnly, Platform, - SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window, - WindowContext, WindowHandle, WindowId, + FocusEvent, FocusHandle, FocusId, KeyBinding, Keymap, LayoutId, MainThread, MainThreadOnly, + Platform, SubscriberSet, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, + Window, WindowContext, WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; use futures::Future; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use slotmap::SlotMap; use std::{ any::{type_name, Any, TypeId}, @@ -67,6 +67,7 @@ impl App { unit_entity, entities, windows: SlotMap::with_key(), + keymap: Arc::new(RwLock::new(Keymap::default())), pending_notifications: Default::default(), pending_effects: Default::default(), observers: SubscriberSet::new(), @@ -111,6 +112,7 @@ pub struct AppContext { pub(crate) unit_entity: Handle<()>, pub(crate) entities: EntityMap, pub(crate) windows: SlotMap>, + keymap: Arc>, pub(crate) pending_notifications: HashSet, pending_effects: VecDeque, pub(crate) observers: SubscriberSet, @@ -403,6 +405,14 @@ impl AppContext { pub(crate) fn pop_text_style(&mut self) { self.text_style_stack.pop(); } + + pub fn bind_keys(&mut self, bindings: impl IntoIterator) { + self.keymap.write().add_bindings(bindings); + let window_ids = self.windows.keys().collect::>(); + for window_id in window_ids { + self.update_window(window_id, |cx| cx.notify()).unwrap(); + } + } } impl Context for AppContext { diff --git a/crates/gpui3/src/gpui3.rs b/crates/gpui3/src/gpui3.rs index 1af9a1cc222386f812fbf1e9c6d535bfafc968e8..fbc9d8d0f2af3141d3b181828d2ed0b9ea5f8d4d 100644 --- a/crates/gpui3/src/gpui3.rs +++ b/crates/gpui3/src/gpui3.rs @@ -1,3 +1,4 @@ +mod action; mod active; mod app; mod assets; @@ -24,6 +25,7 @@ mod util; mod view; mod window; +pub use action::*; pub use active::*; pub use anyhow::Result; pub use app::*; diff --git a/crates/gpui3/src/keymap/binding.rs b/crates/gpui3/src/keymap/binding.rs index e63479ab168f7a119f8957d6dcb8e260def97546..2ac3c10964ec8c48b91e6c7c696e8ab5063e81ca 100644 --- a/crates/gpui3/src/keymap/binding.rs +++ b/crates/gpui3/src/keymap/binding.rs @@ -1,21 +1,21 @@ -use crate::{Action, KeyMatch, KeymapContext, KeymapContextPredicate, Keystroke}; +use crate::{Action, ActionContext, ActionContextPredicate, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; -pub struct Binding { +pub struct KeyBinding { action: Box, pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(super) context_predicate: Option, } -impl Binding { +impl KeyBinding { pub fn new(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self { Self::load(keystrokes, Box::new(action), context_predicate).unwrap() } pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { let context = if let Some(context) = context { - Some(KeymapContextPredicate::parse(context)?) + Some(ActionContextPredicate::parse(context)?) } else { None }; @@ -32,7 +32,7 @@ impl Binding { }) } - pub fn matches_context(&self, contexts: &[KeymapContext]) -> bool { + pub fn matches_context(&self, contexts: &[ActionContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +42,7 @@ impl Binding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[KeymapContext], + contexts: &[ActionContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +61,7 @@ impl Binding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeymapContext], + contexts: &[ActionContext], ) -> Option> { if self.action.partial_eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) diff --git a/crates/gpui3/src/keymap/keymap.rs b/crates/gpui3/src/keymap/keymap.rs index 6bbb87a0e3ae9865644e4e058f8c116fdad10cf3..fffd7d54846570271ea293c956609f4580368348 100644 --- a/crates/gpui3/src/keymap/keymap.rs +++ b/crates/gpui3/src/keymap/keymap.rs @@ -1,4 +1,4 @@ -use crate::{Binding, KeymapContextPredicate, Keystroke}; +use crate::{ActionContextPredicate, KeyBinding, Keystroke}; use collections::HashSet; use smallvec::SmallVec; use std::{any::TypeId, collections::HashMap}; @@ -8,14 +8,14 @@ pub struct KeymapVersion(usize); #[derive(Default)] pub struct Keymap { - bindings: Vec, + bindings: Vec, binding_indices_by_action_id: HashMap>, - disabled_keystrokes: HashMap, HashSet>>, + disabled_keystrokes: HashMap, HashSet>>, version: KeymapVersion, } impl Keymap { - pub fn new(bindings: Vec) -> Self { + pub fn new(bindings: Vec) -> Self { let mut this = Self::default(); this.add_bindings(bindings); this @@ -25,7 +25,7 @@ impl Keymap { self.version } - pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { + pub fn bindings_for_action(&self, action_id: TypeId) -> impl Iterator { self.binding_indices_by_action_id .get(&action_id) .map(SmallVec::as_slice) @@ -35,7 +35,7 @@ impl Keymap { .filter(|binding| !self.binding_disabled(binding)) } - pub fn add_bindings>(&mut self, bindings: T) { + pub fn add_bindings>(&mut self, bindings: T) { // todo!("no action") // let no_action_id = (NoAction {}).id(); let mut new_bindings = Vec::new(); @@ -87,14 +87,14 @@ impl Keymap { self.version.0 += 1; } - pub fn bindings(&self) -> Vec<&Binding> { + pub fn bindings(&self) -> Vec<&KeyBinding> { self.bindings .iter() .filter(|binding| !self.binding_disabled(binding)) .collect() } - fn binding_disabled(&self, binding: &Binding) -> bool { + fn binding_disabled(&self, binding: &KeyBinding) -> bool { match self.disabled_keystrokes.get(&binding.keystrokes) { Some(disabled_predicates) => disabled_predicates.contains(&binding.context_predicate), None => false, diff --git a/crates/gpui3/src/keymap/matcher.rs b/crates/gpui3/src/keymap/matcher.rs index 38d2f38029726561dabede829957b3c78fde99fa..1718a871f99f87553e1ad58c8b49f79b0e4f201f 100644 --- a/crates/gpui3/src/keymap/matcher.rs +++ b/crates/gpui3/src/keymap/matcher.rs @@ -1,13 +1,7 @@ -use crate::{Keymap, KeymapContext, KeymapVersion, Keystroke}; +use crate::{Action, ActionContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::RwLock; use smallvec::SmallVec; -use std::{any::Any, sync::Arc}; - -pub trait Action: Any + Send + Sync { - fn partial_eq(&self, action: &dyn Action) -> bool; - fn boxed_clone(&self) -> Box; - fn as_any(&self) -> &dyn Any; -} +use std::sync::Arc; pub struct KeyMatcher { pending_keystrokes: Vec, @@ -50,7 +44,7 @@ impl KeyMatcher { pub fn match_keystroke( &mut self, keystroke: &Keystroke, - context_stack: &[KeymapContext], + context_stack: &[ActionContext], ) -> KeyMatch { let keymap = self.keymap.read(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. @@ -92,7 +86,7 @@ impl KeyMatcher { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeymapContext], + contexts: &[ActionContext], ) -> Option> { self.keymap .read() @@ -120,7 +114,7 @@ impl KeyMatch { // use anyhow::Result; // use serde::Deserialize; -// use crate::{actions, impl_actions, keymap_matcher::KeymapContext}; +// use crate::{actions, impl_actions, keymap_matcher::ActionContext}; // use super::*; @@ -128,10 +122,10 @@ impl KeyMatch { // fn test_keymap_and_view_ordering() -> Result<()> { // actions!(test, [EditorAction, ProjectPanelAction]); -// let mut editor = KeymapContext::default(); +// let mut editor = ActionContext::default(); // editor.add_identifier("Editor"); -// let mut project_panel = KeymapContext::default(); +// let mut project_panel = ActionContext::default(); // project_panel.add_identifier("ProjectPanel"); // // Editor 'deeper' in than project panel @@ -160,10 +154,10 @@ impl KeyMatch { // fn test_push_keystroke() -> Result<()> { // actions!(test, [B, AB, C, D, DA, E, EF]); -// let mut context1 = KeymapContext::default(); +// let mut context1 = ActionContext::default(); // context1.add_identifier("1"); -// let mut context2 = KeymapContext::default(); +// let mut context2 = ActionContext::default(); // context2.add_identifier("2"); // let dispatch_path = vec![(2, context2), (1, context1)]; @@ -300,27 +294,27 @@ impl KeyMatch { // fn test_context_predicate_eval() { // let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap(); -// let mut context = KeymapContext::default(); +// let mut context = ActionContext::default(); // context.add_identifier("a"); // assert!(!predicate.eval(&[context])); -// let mut context = KeymapContext::default(); +// let mut context = ActionContext::default(); // context.add_identifier("a"); // context.add_identifier("b"); // assert!(predicate.eval(&[context])); -// let mut context = KeymapContext::default(); +// let mut context = ActionContext::default(); // context.add_identifier("a"); // context.add_key("c", "x"); // assert!(!predicate.eval(&[context])); -// let mut context = KeymapContext::default(); +// let mut context = ActionContext::default(); // context.add_identifier("a"); // context.add_key("c", "d"); // assert!(predicate.eval(&[context])); // let predicate = KeymapContextPredicate::parse("!a").unwrap(); -// assert!(predicate.eval(&[KeymapContext::default()])); +// assert!(predicate.eval(&[ActionContext::default()])); // } // #[test] @@ -355,8 +349,8 @@ impl KeyMatch { // assert!(!predicate.eval(&contexts[5..])); // assert!(!predicate.eval(&contexts[6..])); -// fn context_set(names: &[&str]) -> KeymapContext { -// let mut keymap = KeymapContext::new(); +// fn context_set(names: &[&str]) -> ActionContext { +// let mut keymap = ActionContext::new(); // names // .iter() // .for_each(|name| keymap.add_identifier(name.to_string())); @@ -386,10 +380,10 @@ impl KeyMatch { // Binding::new("ctrl-`", Backtick, Some("a")), // ]); -// let mut context_a = KeymapContext::default(); +// let mut context_a = ActionContext::default(); // context_a.add_identifier("a"); -// let mut context_b = KeymapContext::default(); +// let mut context_b = ActionContext::default(); // context_b.add_identifier("b"); // let mut matcher = KeymapMatcher::new(keymap); @@ -434,7 +428,7 @@ impl KeyMatch { // ); // matcher.clear_pending(); -// let mut context_c = KeymapContext::default(); +// let mut context_c = ActionContext::default(); // context_c.add_identifier("c"); // // Pending keystrokes are maintained per-view diff --git a/crates/gpui3/src/keymap/mod.rs b/crates/gpui3/src/keymap/mod.rs index 26fcfac477fb73a70f6151ea4c9d2ea155d88d95..449b5427bf288dbf7647ed3ebd2451e15b10dd15 100644 --- a/crates/gpui3/src/keymap/mod.rs +++ b/crates/gpui3/src/keymap/mod.rs @@ -1,9 +1,7 @@ mod binding; mod keymap; -mod keymap_context; mod matcher; pub use binding::*; pub use keymap::*; -pub use keymap_context::*; pub use matcher::*; From 2b90b8d6b75e50122e4969bcc704f621abc15d87 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:43:28 +0200 Subject: [PATCH 13/20] Checkpoint --- crates/gpui3/src/action.rs | 2 +- crates/gpui3/src/app.rs | 2 +- crates/gpui3/src/element.rs | 2 +- crates/gpui3/src/keymap/binding.rs | 2 +- crates/gpui3/src/window.rs | 17 +++++++- crates/storybook2/src/stories/focus.rs | 59 +++++++++++++++++++++++++- 6 files changed, 77 insertions(+), 7 deletions(-) diff --git a/crates/gpui3/src/action.rs b/crates/gpui3/src/action.rs index e8d113532dc59d9a24eda50f725af800065bdd92..6a21aee95c34883c150ccbfb42a26ef449f82574 100644 --- a/crates/gpui3/src/action.rs +++ b/crates/gpui3/src/action.rs @@ -4,7 +4,7 @@ use collections::{HashMap, HashSet}; use std::any::Any; pub trait Action: Any + Send + Sync { - fn partial_eq(&self, action: &dyn Action) -> bool; + fn eq(&self, action: &dyn Action) -> bool; fn boxed_clone(&self) -> Box; fn as_any(&self) -> &dyn Any; } diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs index c417bcb0dd8955f9fbb2341b6cc3739288d519f9..23aac6526f0168bd19cc5c46d030e167a59c9482 100644 --- a/crates/gpui3/src/app.rs +++ b/crates/gpui3/src/app.rs @@ -112,7 +112,7 @@ pub struct AppContext { pub(crate) unit_entity: Handle<()>, pub(crate) entities: EntityMap, pub(crate) windows: SlotMap>, - keymap: Arc>, + pub(crate) keymap: Arc>, pub(crate) pending_notifications: HashSet, pending_effects: VecDeque, pub(crate) observers: SubscriberSet, diff --git a/crates/gpui3/src/element.rs b/crates/gpui3/src/element.rs index 3a9c179cf9355c04bc879da2d36d8a2b60f0e5a8..20832a1ea38493e0bcef0e1a630869872b11d8b7 100644 --- a/crates/gpui3/src/element.rs +++ b/crates/gpui3/src/element.rs @@ -33,7 +33,7 @@ pub trait Element: 'static + Send + Sync + IntoAnyElement { } #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] -pub struct GlobalElementId(SmallVec<[ElementId; 8]>); +pub struct GlobalElementId(SmallVec<[ElementId; 32]>); pub trait ElementIdentity: 'static + Send + Sync { fn id(&self) -> Option; diff --git a/crates/gpui3/src/keymap/binding.rs b/crates/gpui3/src/keymap/binding.rs index 2ac3c10964ec8c48b91e6c7c696e8ab5063e81ca..2860ef52e78f8e040b68301edc228a2158b30c71 100644 --- a/crates/gpui3/src/keymap/binding.rs +++ b/crates/gpui3/src/keymap/binding.rs @@ -63,7 +63,7 @@ impl KeyBinding { action: &dyn Action, contexts: &[ActionContext], ) -> Option> { - if self.action.partial_eq(action) && self.matches_context(contexts) { + if self.action.eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) } else { None diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index 02160d68329e8687cd9fadb447f906e20e1e81b5..0f818bb4f09904a075d2dbef65cc9da4e1ca37e9 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -1035,8 +1035,21 @@ pub trait BorrowWindow: BorrowAppContext { id: impl Into, f: impl FnOnce(GlobalElementId, &mut Self) -> R, ) -> R { - self.window_mut().element_id_stack.push(id.into()); - let global_id = self.window_mut().element_id_stack.clone(); + let keymap = self.app_mut().keymap.clone(); + let window = self.window_mut(); + window.element_id_stack.push(id.into()); + let global_id = window.element_id_stack.clone(); + + if window.key_matchers.get(&global_id).is_none() { + window.key_matchers.insert( + global_id.clone(), + window + .prev_frame_key_matchers + .remove(&global_id) + .unwrap_or_else(|| KeyMatcher::new(keymap)), + ); + } + let result = f(global_id, self); self.window_mut().element_id_stack.pop(); result diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 3b1d08264c084a643a6100fd0e0f1e783236010c..3f75631cdac9b9cbdfa453b7827a1ff9ef30f30b 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,13 +1,56 @@ -use gpui3::{div, view, Context, Focus, ParentElement, Styled, View, WindowContext}; +use std::any::Any; + +use gpui3::{ + div, view, Action, Context, Focus, Interactive, KeyBinding, ParentElement, Styled, View, + WindowContext, +}; use crate::themes::rose_pine; +#[derive(Clone)] +struct ActionA; + +impl Action for ActionA { + fn eq(&self, action: &dyn Action) -> bool { + action.as_any().downcast_ref::().is_some() + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Clone)] +struct ActionB; + +impl Action for ActionB { + fn eq(&self, action: &dyn Action) -> bool { + action.as_any().downcast_ref::().is_some() + } + + fn boxed_clone(&self) -> Box { + Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + pub struct FocusStory { text: View<()>, } impl FocusStory { pub fn view(cx: &mut WindowContext) -> View<()> { + cx.bind_keys([ + KeyBinding::new("cmd-a", ActionA, None), + KeyBinding::new("cmd-b", ActionB, None), + ]); let theme = rose_pine(); let color_1 = theme.lowest.negative.default.foreground; @@ -22,6 +65,12 @@ impl FocusStory { let child_2 = cx.focus_handle(); view(cx.entity(|cx| ()), move |_, cx| { div() + .on_action(|_, action: &ActionA, phase, cx| { + println!("Action A dispatched on parent during {:?}", phase); + }) + .on_action(|_, action: &ActionB, phase, cx| { + println!("Action A dispatched on parent during {:?}", phase); + }) .focusable(&parent) .on_focus(|_, _, _| println!("Parent focused")) .on_blur(|_, _, _| println!("Parent blurred")) @@ -39,6 +88,10 @@ impl FocusStory { .focus_in(|style| style.bg(color_3)) .child( div() + .id("child 1") + .on_action(|_, action: &ActionA, phase, cx| { + println!("Action A dispatched on child 1 during {:?}", phase); + }) .focusable(&child_1) .w_full() .h_6() @@ -59,6 +112,10 @@ impl FocusStory { ) .child( div() + .id("child 2") + .on_action(|_, action: &ActionB, phase, cx| { + println!("Action B dispatched on child 2 during {:?}", phase); + }) .focusable(&child_2) .w_full() .h_6() From 1343ea66c910295ba6a2f767a5b57afbfce9a82e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:47:50 +0200 Subject: [PATCH 14/20] Checkpoint --- crates/gpui3/src/app.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/gpui3/src/app.rs b/crates/gpui3/src/app.rs index 23aac6526f0168bd19cc5c46d030e167a59c9482..da69599e37035f00bba2da611eda4d2419d68328 100644 --- a/crates/gpui3/src/app.rs +++ b/crates/gpui3/src/app.rs @@ -167,6 +167,7 @@ impl AppContext { } Effect::Emit { .. } => self.pending_effects.push_back(effect), Effect::FocusChanged { .. } => self.pending_effects.push_back(effect), + Effect::Refresh => self.pending_effects.push_back(effect), } } @@ -181,6 +182,9 @@ impl AppContext { Effect::FocusChanged { window_id, focused } => { self.apply_focus_changed(window_id, focused) } + Effect::Refresh => { + self.apply_refresh(); + } } } else { break; @@ -286,6 +290,14 @@ impl AppContext { .ok(); } + pub fn apply_refresh(&mut self) { + for window in self.windows.values_mut() { + if let Some(window) = window.as_mut() { + window.dirty = true; + } + } + } + pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext(unsafe { mem::transmute(self.this.clone()) }) } @@ -408,10 +420,7 @@ impl AppContext { pub fn bind_keys(&mut self, bindings: impl IntoIterator) { self.keymap.write().add_bindings(bindings); - let window_ids = self.windows.keys().collect::>(); - for window_id in window_ids { - self.update_window(window_id, |cx| cx.notify()).unwrap(); - } + self.push_effect(Effect::Refresh); } } @@ -502,6 +511,7 @@ pub(crate) enum Effect { window_id: WindowId, focused: Option, }, + Refresh, } #[cfg(test)] From 9e20ccc01aa19ff1696e58ee1ce2654bff9d4e74 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 19 Oct 2023 19:50:44 +0200 Subject: [PATCH 15/20] Checkpoint --- crates/gpui3/src/window.rs | 4 ++-- crates/storybook2/src/stories/focus.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui3/src/window.rs b/crates/gpui3/src/window.rs index 0f818bb4f09904a075d2dbef65cc9da4e1ca37e9..5a68e0dfd2a55fe343a57435084e1bd68fa4767a 100644 --- a/crates/gpui3/src/window.rs +++ b/crates/gpui3/src/window.rs @@ -945,7 +945,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { } fn dispatch_action(&mut self, action: Box, listeners: &[(TypeId, AnyKeyListener)]) { - let action_type = action.type_id(); + let action_type = action.as_any().type_id(); for (event_type, listener) in listeners { if action_type == *event_type { listener(action.as_any(), DispatchPhase::Capture, self); @@ -958,7 +958,7 @@ impl<'a, 'w> WindowContext<'a, 'w> { if self.window.propagate { for (event_type, listener) in listeners.iter().rev() { if action_type == *event_type { - listener(action.as_any(), DispatchPhase::Capture, self); + listener(action.as_any(), DispatchPhase::Bubble, self); if !self.window.propagate { break; } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 3f75631cdac9b9cbdfa453b7827a1ff9ef30f30b..b10c75c1900df614df9d45d2a56b02d17d4bf932 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -69,7 +69,7 @@ impl FocusStory { println!("Action A dispatched on parent during {:?}", phase); }) .on_action(|_, action: &ActionB, phase, cx| { - println!("Action A dispatched on parent during {:?}", phase); + println!("Action B dispatched on parent during {:?}", phase); }) .focusable(&parent) .on_focus(|_, _, _| println!("Parent focused")) From e657e4d1d1dfabe40e097827745a2784b6932872 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 14:06:31 -0400 Subject: [PATCH 16/20] Wire up livestream debug toggle --- crates/ui2/src/components/title_bar.rs | 15 +++++-------- crates/ui2/src/components/workspace.rs | 31 +++++++++++++------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/crates/ui2/src/components/title_bar.rs b/crates/ui2/src/components/title_bar.rs index 4472c8cc6e7e0eec34b94316e42bb23d4859f82c..6d2a3164314c14a9a5457a99568ea22d8fb87b0e 100644 --- a/crates/ui2/src/components/title_bar.rs +++ b/crates/ui2/src/components/title_bar.rs @@ -6,7 +6,7 @@ use gpui3::{view, Context, View}; use crate::prelude::*; use crate::settings::user_settings; use crate::{ - random_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, MicStatus, + theme, Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus, ScreenShareStatus, ToolDivider, TrafficLights, }; @@ -80,14 +80,9 @@ impl TitleBar { cx.notify(); } - pub fn view(cx: &mut WindowContext) -> View { + pub fn view(cx: &mut WindowContext, livestream: Option) -> View { view( - cx.entity(|cx| { - Self::new(cx).set_livestream(Some(Livestream { - players: random_players_with_call_status(7), - channel: Some("gpui2-ui".to_string()), - })) - }), + cx.entity(|cx| Self::new(cx).set_livestream(livestream)), Self::render, ) } @@ -133,7 +128,7 @@ impl TitleBar { .child(Button::new("zed")) .child(Button::new("nate/gpui2-ui-components")), ) - // .children(player_list.map(|p| PlayerStack::new(p))) + .children(player_list.map(|p| PlayerStack::new(p))) .child(IconButton::new(Icon::Plus)), ) .child( @@ -204,7 +199,7 @@ mod stories { pub fn view(cx: &mut WindowContext) -> View { view( cx.entity(|cx| Self { - title_bar: TitleBar::view(cx), + title_bar: TitleBar::view(cx, None), }), Self::render, ) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 856244f6a1799697444f7b49f18c68b4e943b3a9..596dd1aa7cf5a802d5a486453ac481a2bd441711 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -3,12 +3,12 @@ use std::sync::Arc; use chrono::DateTime; use gpui3::{px, relative, rems, view, Context, Size, View}; -use crate::settings::FakeSettings; -use crate::{prelude::*, user_settings_mut, Button, SettingValue}; +use crate::prelude::*; use crate::{ - theme, v_stack, AssistantPanel, ChatMessage, ChatPanel, CollabPanel, EditorPane, Label, - LanguageSelector, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, - SplitDirection, StatusBar, Terminal, TitleBar, Toast, ToastOrigin, + static_livestream, theme, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage, + ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Livestream, Pane, + PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, + StatusBar, Terminal, TitleBar, Toast, ToastOrigin, }; #[derive(Clone)] @@ -50,7 +50,7 @@ pub struct Workspace { impl Workspace { pub fn new(cx: &mut ViewContext) -> Self { Self { - title_bar: TitleBar::view(cx), + title_bar: TitleBar::view(cx, None), editor_1: EditorPane::view(cx), show_project_panel: true, show_collab_panel: false, @@ -159,20 +159,19 @@ impl Workspace { } pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext) { - if self.debug.in_livestream { - self.debug.in_livestream = false; - } else { - self.debug.in_livestream = true; - } + self.debug.in_livestream = !self.debug.in_livestream; + + self.title_bar = TitleBar::view( + cx, + Some(static_livestream()).filter(|_| self.debug.in_livestream), + ); + cx.notify(); } pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext) { - if self.debug.show_toast { - self.debug.show_toast = false; - } else { - self.debug.show_toast = true; - } + self.debug.show_toast = !self.debug.show_toast; + cx.notify(); } From 597aa0475e97513d85257e9f81058a168c73b8bf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 14:10:13 -0400 Subject: [PATCH 17/20] Remove unused import --- crates/ui2/src/components/workspace.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 596dd1aa7cf5a802d5a486453ac481a2bd441711..4f038b22f9defa99fc450bfe64b7d6cc8436d27d 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -6,9 +6,9 @@ use gpui3::{px, relative, rems, view, Context, Size, View}; use crate::prelude::*; use crate::{ static_livestream, theme, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage, - ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Livestream, Pane, - PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, - StatusBar, Terminal, TitleBar, Toast, ToastOrigin, + ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup, + Panel, PanelAllowedSides, PanelSide, ProjectPanel, SettingValue, SplitDirection, StatusBar, + Terminal, TitleBar, Toast, ToastOrigin, }; #[derive(Clone)] From 184f5f2397146306711133f6a945bacffdeee6d7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 14:17:35 -0400 Subject: [PATCH 18/20] Restore active styles for `Input`s --- crates/ui2/src/elements/input.rs | 3 ++- crates/ui2/src/prelude.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ui2/src/elements/input.rs b/crates/ui2/src/elements/input.rs index 45edb1a73d7fe469609ce88a506d675abe279880..ea8c3306bf8c3373cc7717722d199bba5ae99201 100644 --- a/crates/ui2/src/elements/input.rs +++ b/crates/ui2/src/elements/input.rs @@ -82,6 +82,7 @@ impl Input { } div() + .id("input") .h_7() .w_full() .px_2() @@ -93,7 +94,7 @@ impl Input { .border_color(border_color_hover) .bg(background_color_active) }) - // .active(|a| .border_color(border_color_active)) + .active(|style| style.border_color(theme.middle.base.active.border)) .flex() .items_center() .child( diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index ef7e0fc5ab60e8b33859b52d468f5388ce4fd506..35b0cb2808da3cd9deed0075923d8685f7f2eb2d 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -1,6 +1,6 @@ pub use gpui3::{ - div, Click, Element, Hover, IntoAnyElement, ParentElement, ScrollState, SharedString, Styled, - ViewContext, WindowContext, + div, Active, Click, Element, Hover, IntoAnyElement, ParentElement, ScrollState, SharedString, + Styled, ViewContext, WindowContext, }; use crate::settings::user_settings; From 743949753a226233ef54bf4a7097958cf29cd196 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 14:19:10 -0400 Subject: [PATCH 19/20] Fix mutual-exclusivity of right panels --- crates/ui2/src/components/workspace.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 4f038b22f9defa99fc450bfe64b7d6cc8436d27d..055f05125d19ddb1ea88526331d2bbe3733e6f9d 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -108,6 +108,7 @@ impl Workspace { self.show_chat_panel = !self.show_chat_panel; self.show_assistant_panel = false; + self.show_notifications_panel = false; cx.notify(); } @@ -119,7 +120,8 @@ impl Workspace { pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext) { self.show_notifications_panel = !self.show_notifications_panel; - self.show_notifications_panel = false; + self.show_chat_panel = false; + self.show_assistant_panel = false; cx.notify(); } @@ -132,6 +134,7 @@ impl Workspace { self.show_assistant_panel = !self.show_assistant_panel; self.show_chat_panel = false; + self.show_notifications_panel = false; cx.notify(); } From d5fc8313211e913152163b8d0fad524c89157b84 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 19 Oct 2023 14:23:45 -0400 Subject: [PATCH 20/20] Restore more active styles --- crates/ui2/src/components/collab_panel.rs | 3 ++- crates/ui2/src/components/palette.rs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ui2/src/components/collab_panel.rs b/crates/ui2/src/components/collab_panel.rs index 55733ed8d360d332f222bbaea7815289b42d78a7..caf900d8e650d51b1836b822e9d7c4e034171437 100644 --- a/crates/ui2/src/components/collab_panel.rs +++ b/crates/ui2/src/components/collab_panel.rs @@ -133,12 +133,13 @@ impl CollabPanel { theme: &Theme, ) -> impl Element { div() + .id("list_item") .h_7() .px_2() .flex() .items_center() .hover(|style| style.bg(theme.lowest.variant.hovered.background)) - // .active(|style| style.fill(theme.lowest.variant.pressed.background)) + .active(|style| style.bg(theme.lowest.variant.pressed.background)) .child( div() .flex() diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4ed99d295f5bcd9b659080e06867943a769b73bd..a080f85938c290ed29be720e3d81b479358e6ad7 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -84,15 +84,15 @@ impl Palette { .into_iter() .flatten(), ) - .children(self.items.iter().map(|item| { + .children(self.items.iter().enumerate().map(|(index, item)| { h_stack() + .id(index) .justify_between() .px_2() .py_0p5() .rounded_lg() .hover(|style| style.bg(theme.lowest.base.hovered.background)) - // .active() - // .fill(theme.lowest.base.pressed.background) + .active(|style| style.bg(theme.lowest.base.pressed.background)) .child(item.clone()) })), ),