onboarding: Actions for page navigation (#35484)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

assets/keymaps/default-linux.json    |   9 +
assets/keymaps/default-macos.json    |   9 +
crates/onboarding/src/basics_page.rs |   4 
crates/onboarding/src/onboarding.rs  | 158 +++++++++++++++++------------
crates/zed/src/zed.rs                |   1 
5 files changed, 111 insertions(+), 70 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -1168,5 +1168,14 @@
       "up": "menu::SelectPrevious",
       "down": "menu::SelectNext"
     }
+  },
+  {
+    "context": "Onboarding",
+    "use_key_equivalents": true,
+    "bindings": {
+      "ctrl-1": "onboarding::ActivateBasicsPage",
+      "ctrl-2": "onboarding::ActivateEditingPage",
+      "ctrl-3": "onboarding::ActivateAISetupPage"
+    }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -1270,5 +1270,14 @@
       "up": "menu::SelectPrevious",
       "down": "menu::SelectNext"
     }
+  },
+  {
+    "context": "Onboarding",
+    "use_key_equivalents": true,
+    "bindings": {
+      "cmd-1": "onboarding::ActivateBasicsPage",
+      "cmd-2": "onboarding::ActivateEditingPage",
+      "cmd-3": "onboarding::ActivateAISetupPage"
+    }
   }
 ]

crates/onboarding/src/basics_page.rs 🔗

@@ -153,10 +153,8 @@ fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
         new_appearance: Appearance,
         cx: &mut App,
     ) {
-        appearance_state.update(cx, |appearance, _| {
-            *appearance = new_appearance;
-        });
         let fs = <dyn Fs>::global(cx);
+        appearance_state.write(cx, new_appearance);
 
         update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
             if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) {

crates/onboarding/src/onboarding.rs 🔗

@@ -6,8 +6,8 @@ use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
 use fs::Fs;
 use gpui::{
     Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
-    FocusHandle, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity,
-    Window, actions,
+    FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task,
+    WeakEntity, Window, actions,
 };
 use schemars::JsonSchema;
 use serde::Deserialize;
@@ -65,6 +65,18 @@ actions!(
     ]
 );
 
+actions!(
+    onboarding,
+    [
+        /// Activates the Basics page.
+        ActivateBasicsPage,
+        /// Activates the Editing page.
+        ActivateEditingPage,
+        /// Activates the AI Setup page.
+        ActivateAISetupPage,
+    ]
+);
+
 pub fn init(cx: &mut App) {
     cx.on_action(|_: &OpenOnboarding, cx| {
         with_active_or_new_workspace(cx, |workspace, window, cx| {
@@ -235,67 +247,69 @@ impl Onboarding {
         })
     }
 
-    fn render_nav_button(
+    fn render_nav_buttons(
         &mut self,
-        page: SelectedPage,
-        _: &mut Window,
+        window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let text = match page {
-            SelectedPage::Basics => "Basics",
-            SelectedPage::Editing => "Editing",
-            SelectedPage::AiSetup => "AI Setup",
-        };
+    ) -> [impl IntoElement; 3] {
+        let pages = [
+            SelectedPage::Basics,
+            SelectedPage::Editing,
+            SelectedPage::AiSetup,
+        ];
 
-        let binding = match page {
-            SelectedPage::Basics => {
-                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
-                    .map(|kb| kb.size(rems_from_px(12.)))
-            }
-            SelectedPage::Editing => {
-                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
-                    .map(|kb| kb.size(rems_from_px(12.)))
-            }
-            SelectedPage::AiSetup => {
-                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
-                    .map(|kb| kb.size(rems_from_px(12.)))
-            }
-        };
+        let text = ["Basics", "Editing", "AI Setup"];
 
-        let selected = self.selected_page == page;
+        let actions: [&dyn Action; 3] = [
+            &ActivateBasicsPage,
+            &ActivateEditingPage,
+            &ActivateAISetupPage,
+        ];
 
-        h_flex()
-            .id(text)
-            .relative()
-            .w_full()
-            .gap_2()
-            .px_2()
-            .py_0p5()
-            .justify_between()
-            .rounded_sm()
-            .when(selected, |this| {
-                this.child(
-                    div()
-                        .h_4()
-                        .w_px()
-                        .bg(cx.theme().colors().text_accent)
-                        .absolute()
-                        .left_0(),
-                )
-            })
-            .hover(|style| style.bg(cx.theme().colors().element_hover))
-            .child(Label::new(text).map(|this| {
-                if selected {
-                    this.color(Color::Default)
-                } else {
-                    this.color(Color::Muted)
-                }
-            }))
-            .child(binding)
-            .on_click(cx.listener(move |this, _, _, cx| {
-                this.selected_page = page;
-                cx.notify();
-            }))
+        let mut binding = actions.map(|action| {
+            KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
+                .map(|kb| kb.size(rems_from_px(12.)))
+        });
+
+        pages.map(|page| {
+            let i = page as usize;
+            let selected = self.selected_page == page;
+            h_flex()
+                .id(text[i])
+                .relative()
+                .w_full()
+                .gap_2()
+                .px_2()
+                .py_0p5()
+                .justify_between()
+                .rounded_sm()
+                .when(selected, |this| {
+                    this.child(
+                        div()
+                            .h_4()
+                            .w_px()
+                            .bg(cx.theme().colors().text_accent)
+                            .absolute()
+                            .left_0(),
+                    )
+                })
+                .hover(|style| style.bg(cx.theme().colors().element_hover))
+                .child(Label::new(text[i]).map(|this| {
+                    if selected {
+                        this.color(Color::Default)
+                    } else {
+                        this.color(Color::Muted)
+                    }
+                }))
+                .child(binding[i].take().map_or(
+                    gpui::Empty.into_any_element(),
+                    IntoElement::into_any_element,
+                ))
+                .on_click(cx.listener(move |this, _, _, cx| {
+                    this.selected_page = page;
+                    cx.notify();
+                }))
+        })
     }
 
     fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -335,14 +349,7 @@ impl Onboarding {
                                     .border_y_1()
                                     .border_color(cx.theme().colors().border_variant.opacity(0.5))
                                     .gap_1()
-                                    .children([
-                                        self.render_nav_button(SelectedPage::Basics, window, cx)
-                                            .into_element(),
-                                        self.render_nav_button(SelectedPage::Editing, window, cx)
-                                            .into_element(),
-                                        self.render_nav_button(SelectedPage::AiSetup, window, cx)
-                                            .into_element(),
-                                    ]),
+                                    .children(self.render_nav_buttons(window, cx)),
                             )
                             .child(
                                 ButtonLike::new("skip_all")
@@ -454,9 +461,26 @@ impl Render for Onboarding {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         h_flex()
             .image_cache(gpui::retain_all("onboarding-page"))
-            .key_context("onboarding-page")
+            .key_context({
+                let mut ctx = KeyContext::new_with_defaults();
+                ctx.add("Onboarding");
+                ctx
+            })
+            .track_focus(&self.focus_handle)
             .size_full()
             .bg(cx.theme().colors().editor_background)
+            .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
+                this.selected_page = SelectedPage::Basics;
+                cx.notify();
+            }))
+            .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
+                this.selected_page = SelectedPage::Editing;
+                cx.notify();
+            }))
+            .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
+                this.selected_page = SelectedPage::AiSetup;
+                cx.notify();
+            }))
             .child(
                 h_flex()
                     .max_w(rems_from_px(1100.))

crates/zed/src/zed.rs 🔗

@@ -4354,6 +4354,7 @@ mod tests {
                 "menu",
                 "notebook",
                 "notification_panel",
+                "onboarding",
                 "outline",
                 "outline_panel",
                 "pane",