More UI

Nate Butler created

Change summary

crates/onboarding_ui/src/onboarding_ui.rs | 270 ++++++++++++++++++++-----
1 file changed, 216 insertions(+), 54 deletions(-)

Detailed changes

crates/onboarding_ui/src/onboarding_ui.rs 🔗

@@ -1,12 +1,16 @@
 #![allow(unused, dead_code)]
+use client::Client;
 use command_palette_hooks::CommandPaletteFilter;
 use feature_flags::FeatureFlagAppExt as _;
 use gpui::{Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, actions, prelude::*};
 use settings_ui::SettingsUiFeatureFlag;
-use ui::prelude::*;
+use std::sync::Arc;
+use ui::{KeyBinding, ListItem, Vector, VectorName, prelude::*};
+use util::ResultExt;
 use workspace::{
     Workspace, WorkspaceId,
     item::{Item, ItemEvent},
+    notifications::NotifyResultExt,
 };
 
 actions!(
@@ -27,41 +31,10 @@ actions!(
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, _, _cx| {
         workspace.register_action(|workspace, _: &ShowOnboarding, window, cx| {
-            let onboarding = cx.new(|cx| OnboardingUI::new(workspace, cx));
+            let client = workspace.client().clone();
+            let onboarding = cx.new(|cx| OnboardingUI::new(workspace, client, cx));
             workspace.add_item_to_active_pane(Box::new(onboarding), None, true, window, cx);
         });
-
-        workspace.register_action(|_workspace, _: &JumpToBasics, _window, _cx| {
-            // Jump to basics implementation
-        });
-
-        workspace.register_action(|_workspace, _: &JumpToEditing, _window, _cx| {
-            // Jump to editing implementation
-        });
-
-        workspace.register_action(|_workspace, _: &JumpToAiSetup, _window, _cx| {
-            // Jump to AI setup implementation
-        });
-
-        workspace.register_action(|_workspace, _: &JumpToWelcome, _window, _cx| {
-            // Jump to welcome implementation
-        });
-
-        workspace.register_action(|_workspace, _: &NextPage, _window, _cx| {
-            // Next page implementation
-        });
-
-        workspace.register_action(|_workspace, _: &PreviousPage, _window, _cx| {
-            // Previous page implementation
-        });
-
-        workspace.register_action(|_workspace, _: &ToggleFocus, _window, _cx| {
-            // Toggle focus implementation
-        });
-
-        workspace.register_action(|_workspace, _: &ResetOnboarding, _window, _cx| {
-            // Reset onboarding implementation
-        });
     })
     .detach();
 
@@ -131,6 +104,7 @@ pub struct OnboardingUI {
 
     // Workspace reference for Item trait
     workspace: WeakEntity<Workspace>,
+    client: Arc<Client>,
 }
 
 impl EventEmitter<ItemEvent> for OnboardingUI {}
@@ -152,26 +126,47 @@ impl Render for OnboardingUI {
         window: &mut gpui::Window,
         cx: &mut Context<Self>,
     ) -> impl gpui::IntoElement {
-        h_flex()
-            .id("onboarding-ui")
-            .key_context("Onboarding")
-            .track_focus(&self.focus_handle)
-            .w(px(904.))
-            .h(px(500.))
-            .gap(px(48.))
-            .child(v_flex().h_full().w(px(256.)).child("nav"))
-            .child(self.render_active_page(window, cx))
+        div()
+            .bg(cx.theme().colors().editor_background)
+            .size_full()
+            .flex()
+            .items_center()
+            .justify_center()
+            .overflow_hidden()
+            .child(
+                h_flex()
+                    .id("onboarding-ui")
+                    .key_context("Onboarding")
+                    .track_focus(&self.focus_handle)
+                    .on_action(cx.listener(Self::handle_jump_to_basics))
+                    .on_action(cx.listener(Self::handle_jump_to_editing))
+                    .on_action(cx.listener(Self::handle_jump_to_ai_setup))
+                    .on_action(cx.listener(Self::handle_jump_to_welcome))
+                    .on_action(cx.listener(Self::handle_next_page))
+                    .on_action(cx.listener(Self::handle_previous_page))
+                    .w(px(904.))
+                    .h(px(500.))
+                    .gap(px(48.))
+                    .child(self.render_navigation(window, cx))
+                    .child(
+                        v_flex()
+                            .h_full()
+                            .flex_1()
+                            .child(div().flex_1().child(self.render_active_page(window, cx))),
+                    ),
+            )
     }
 }
 
 impl OnboardingUI {
-    pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
+    pub fn new(workspace: &Workspace, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
         Self {
             focus_handle: cx.focus_handle(),
             current_page: OnboardingPage::Basics,
             current_focus: OnboardingFocus::Page,
             completed_pages: [false; 4],
             workspace: workspace.weak_handle(),
+            client,
         }
     }
 
@@ -182,6 +177,7 @@ impl OnboardingUI {
         cx: &mut Context<Self>,
     ) {
         self.current_page = page;
+        cx.emit(ItemEvent::UpdateTab);
         cx.notify();
     }
 
@@ -230,11 +226,142 @@ impl OnboardingUI {
         cx.notify();
     }
 
+    fn handle_jump_to_basics(
+        &mut self,
+        _: &JumpToBasics,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.jump_to_page(OnboardingPage::Basics, window, cx);
+    }
+
+    fn handle_jump_to_editing(
+        &mut self,
+        _: &JumpToEditing,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.jump_to_page(OnboardingPage::Editing, window, cx);
+    }
+
+    fn handle_jump_to_ai_setup(
+        &mut self,
+        _: &JumpToAiSetup,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.jump_to_page(OnboardingPage::AiSetup, window, cx);
+    }
+
+    fn handle_jump_to_welcome(
+        &mut self,
+        _: &JumpToWelcome,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.jump_to_page(OnboardingPage::Welcome, window, cx);
+    }
+
+    fn handle_next_page(&mut self, _: &NextPage, window: &mut Window, cx: &mut Context<Self>) {
+        self.next_page(window, cx);
+    }
+
+    fn handle_previous_page(
+        &mut self,
+        _: &PreviousPage,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.previous_page(window, cx);
+    }
+
+    fn render_navigation(
+        &mut self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl gpui::IntoElement {
+        v_flex()
+            .h_full()
+            .w(px(256.))
+            .gap_2()
+            .justify_between()
+            .child(
+                v_flex()
+                    .w_full()
+                    .gap_px()
+                    .child(
+                        h_flex()
+                            .w_full()
+                            .justify_between()
+                            .child(Vector::new(VectorName::ZedLogo, rems(2.), rems(2.)))
+                            .child(self.render_sign_in_button(cx)),
+                    )
+                    .child(self.render_nav_item(OnboardingPage::Basics, "The Basics", "1", cx))
+                    .child(self.render_nav_item(
+                        OnboardingPage::Editing,
+                        "Editing Experience",
+                        "2",
+                        cx,
+                    ))
+                    .child(self.render_nav_item(OnboardingPage::AiSetup, "AI Setup", "3", cx))
+                    .child(self.render_nav_item(OnboardingPage::Welcome, "Welcome", "4", cx)),
+            )
+            .child(self.render_bottom_controls(window, cx))
+    }
+
+    fn render_nav_item(
+        &mut self,
+        page: OnboardingPage,
+        label: impl Into<SharedString>,
+        shortcut: impl Into<SharedString>,
+        cx: &mut Context<Self>,
+    ) -> impl gpui::IntoElement {
+        let selected = self.current_page == page;
+        let label = label.into();
+        let shortcut = shortcut.into();
+
+        ListItem::new(label.clone())
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .child(Label::new(label.clone()))
+                    .child(Label::new(format!("⌘{}", shortcut.clone())).color(Color::Muted)),
+            )
+            .selectable(true)
+            .toggle_state(selected)
+            .on_click(cx.listener(move |this, _, window, cx| {
+                this.jump_to_page(page, window, cx);
+            }))
+    }
+
+    fn render_bottom_controls(
+        &mut self,
+        window: &mut gpui::Window,
+        cx: &mut Context<Self>,
+    ) -> impl gpui::IntoElement {
+        h_flex().w_full().p_4().child(
+            Button::new(
+                "next",
+                if self.current_page == OnboardingPage::Welcome {
+                    "Get Started"
+                } else {
+                    "Next"
+                },
+            )
+            .style(ButtonStyle::Filled)
+            .key_binding(KeyBinding::for_action(&NextPage, window, cx))
+            .on_click(cx.listener(|this, _, window, cx| {
+                this.next_page(window, cx);
+            })),
+        )
+    }
+
     fn render_active_page(
         &mut self,
         _window: &mut gpui::Window,
         _cx: &mut Context<Self>,
-    ) -> impl gpui::IntoElement {
+    ) -> AnyElement {
         match self.current_page {
             OnboardingPage::Basics => self.render_basics_page(),
             OnboardingPage::Editing => self.render_editing_page(),
@@ -243,20 +370,54 @@ impl OnboardingUI {
         }
     }
 
-    fn render_basics_page(&self) -> impl gpui::IntoElement {
-        v_flex().h_full().w_full().child("Basics Page")
+    fn render_basics_page(&self) -> AnyElement {
+        v_flex()
+            .h_full()
+            .w_full()
+            .child("Basics Page")
+            .into_any_element()
+    }
+
+    fn render_editing_page(&self) -> AnyElement {
+        v_flex()
+            .h_full()
+            .w_full()
+            .child("Editing Page")
+            .into_any_element()
     }
 
-    fn render_editing_page(&self) -> impl gpui::IntoElement {
-        v_flex().h_full().w_full().child("Editing Page")
+    fn render_ai_setup_page(&self) -> AnyElement {
+        v_flex()
+            .h_full()
+            .w_full()
+            .child("AI Setup Page")
+            .into_any_element()
     }
 
-    fn render_ai_setup_page(&self) -> impl gpui::IntoElement {
-        v_flex().h_full().w_full().child("AI Setup Page")
+    fn render_welcome_page(&self) -> AnyElement {
+        v_flex()
+            .h_full()
+            .w_full()
+            .child("Welcome Page")
+            .into_any_element()
     }
 
-    fn render_welcome_page(&self) -> impl gpui::IntoElement {
-        v_flex().h_full().w_full().child("Welcome Page")
+    fn render_sign_in_button(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
+        let client = self.client.clone();
+        Button::new("sign_in", "Sign in")
+            .label_size(LabelSize::Small)
+            .on_click(cx.listener(move |_, _, window, cx| {
+                let client = client.clone();
+                window
+                    .spawn(cx, async move |cx| {
+                        client
+                            .authenticate_and_connect(true, &cx)
+                            .await
+                            .into_response()
+                            .notify_async_err(cx);
+                    })
+                    .detach();
+            }))
     }
 }
 
@@ -282,9 +443,10 @@ impl Item for OnboardingUI {
         cx: &mut Context<Self>,
     ) -> Option<Entity<Self>> {
         let weak_workspace = self.workspace.clone();
+        let client = self.client.clone();
         if let Some(workspace) = weak_workspace.upgrade() {
             workspace.update(cx, |workspace, cx| {
-                Some(cx.new(|cx| OnboardingUI::new(workspace, cx)))
+                Some(cx.new(|cx| OnboardingUI::new(workspace, client, cx)))
             })
         } else {
             None