onboarding.rs

  1use crate::welcome::{ShowWelcome, WelcomePage};
  2use command_palette_hooks::CommandPaletteFilter;
  3use db::kvp::KEY_VALUE_STORE;
  4use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
  5use fs::Fs;
  6use gpui::{
  7    AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
  8    IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
  9};
 10use settings::{Settings, SettingsStore, update_settings_file};
 11use std::sync::Arc;
 12use theme::{ThemeMode, ThemeSettings};
 13use ui::{
 14    Divider, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement,
 15    ToggleButtonGroup, ToggleButtonSimple, Vector, VectorName, prelude::*, rems_from_px,
 16};
 17use workspace::{
 18    AppState, Workspace, WorkspaceId,
 19    dock::DockPosition,
 20    item::{Item, ItemEvent},
 21    open_new, with_active_or_new_workspace,
 22};
 23
 24mod editing_page;
 25mod welcome;
 26
 27pub struct OnBoardingFeatureFlag {}
 28
 29impl FeatureFlag for OnBoardingFeatureFlag {
 30    const NAME: &'static str = "onboarding";
 31}
 32
 33pub const FIRST_OPEN: &str = "first_open";
 34
 35actions!(
 36    zed,
 37    [
 38        /// Opens the onboarding view.
 39        OpenOnboarding
 40    ]
 41);
 42
 43pub fn init(cx: &mut App) {
 44    cx.on_action(|_: &OpenOnboarding, cx| {
 45        with_active_or_new_workspace(cx, |workspace, window, cx| {
 46            workspace
 47                .with_local_workspace(window, cx, |workspace, window, cx| {
 48                    let existing = workspace
 49                        .active_pane()
 50                        .read(cx)
 51                        .items()
 52                        .find_map(|item| item.downcast::<Onboarding>());
 53
 54                    if let Some(existing) = existing {
 55                        workspace.activate_item(&existing, true, true, window, cx);
 56                    } else {
 57                        let settings_page = Onboarding::new(workspace.weak_handle(), cx);
 58                        workspace.add_item_to_active_pane(
 59                            Box::new(settings_page),
 60                            None,
 61                            true,
 62                            window,
 63                            cx,
 64                        )
 65                    }
 66                })
 67                .detach();
 68        });
 69    });
 70
 71    cx.on_action(|_: &ShowWelcome, cx| {
 72        with_active_or_new_workspace(cx, |workspace, window, cx| {
 73            workspace
 74                .with_local_workspace(window, cx, |workspace, window, cx| {
 75                    let existing = workspace
 76                        .active_pane()
 77                        .read(cx)
 78                        .items()
 79                        .find_map(|item| item.downcast::<WelcomePage>());
 80
 81                    if let Some(existing) = existing {
 82                        workspace.activate_item(&existing, true, true, window, cx);
 83                    } else {
 84                        let settings_page = WelcomePage::new(window, cx);
 85                        workspace.add_item_to_active_pane(
 86                            Box::new(settings_page),
 87                            None,
 88                            true,
 89                            window,
 90                            cx,
 91                        )
 92                    }
 93                })
 94                .detach();
 95        });
 96    });
 97
 98    cx.observe_new::<Workspace>(|_, window, cx| {
 99        let Some(window) = window else {
100            return;
101        };
102
103        let onboarding_actions = [
104            std::any::TypeId::of::<OpenOnboarding>(),
105            std::any::TypeId::of::<ShowWelcome>(),
106        ];
107
108        CommandPaletteFilter::update_global(cx, |filter, _cx| {
109            filter.hide_action_types(&onboarding_actions);
110        });
111
112        cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
113            if is_enabled {
114                CommandPaletteFilter::update_global(cx, |filter, _cx| {
115                    filter.show_action_types(onboarding_actions.iter());
116                });
117            } else {
118                CommandPaletteFilter::update_global(cx, |filter, _cx| {
119                    filter.hide_action_types(&onboarding_actions);
120                });
121            }
122        })
123        .detach();
124    })
125    .detach();
126}
127
128pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
129    open_new(
130        Default::default(),
131        app_state,
132        cx,
133        |workspace, window, cx| {
134            {
135                workspace.toggle_dock(DockPosition::Left, window, cx);
136                let onboarding_page = Onboarding::new(workspace.weak_handle(), cx);
137                workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
138
139                window.focus(&onboarding_page.focus_handle(cx));
140
141                cx.notify();
142            };
143            db::write_and_log(cx, || {
144                KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
145            });
146        },
147    )
148}
149
150fn read_theme_selection(cx: &App) -> ThemeMode {
151    let settings = ThemeSettings::get_global(cx);
152    settings
153        .theme_selection
154        .as_ref()
155        .and_then(|selection| selection.mode())
156        .unwrap_or_default()
157}
158
159fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
160    let fs = <dyn Fs>::global(cx);
161
162    update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
163        settings.set_mode(theme_mode);
164    });
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168enum SelectedPage {
169    Basics,
170    Editing,
171    AiSetup,
172}
173
174struct Onboarding {
175    workspace: WeakEntity<Workspace>,
176    focus_handle: FocusHandle,
177    selected_page: SelectedPage,
178    _settings_subscription: Subscription,
179}
180
181impl Onboarding {
182    fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> {
183        cx.new(|cx| Self {
184            workspace,
185            focus_handle: cx.focus_handle(),
186            selected_page: SelectedPage::Basics,
187            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
188        })
189    }
190
191    fn render_page_nav(
192        &mut self,
193        page: SelectedPage,
194        _: &mut Window,
195        cx: &mut Context<Self>,
196    ) -> impl IntoElement {
197        let text = match page {
198            SelectedPage::Basics => "Basics",
199            SelectedPage::Editing => "Editing",
200            SelectedPage::AiSetup => "AI Setup",
201        };
202        let binding = match page {
203            SelectedPage::Basics => {
204                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
205            }
206            SelectedPage::Editing => {
207                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
208            }
209            SelectedPage::AiSetup => {
210                KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
211            }
212        };
213        let selected = self.selected_page == page;
214        h_flex()
215            .id(text)
216            .rounded_sm()
217            .child(text)
218            .child(binding)
219            .h_8()
220            .gap_2()
221            .px_2()
222            .py_0p5()
223            .w_full()
224            .justify_between()
225            .map(|this| {
226                if selected {
227                    this.bg(Color::Selected.color(cx))
228                        .border_l_1()
229                        .border_color(Color::Accent.color(cx))
230                } else {
231                    this.text_color(Color::Muted.color(cx))
232                }
233            })
234            .hover(|style| {
235                if selected {
236                    style.bg(Color::Selected.color(cx).opacity(0.6))
237                } else {
238                    style.bg(Color::Selected.color(cx).opacity(0.3))
239                }
240            })
241            .on_click(cx.listener(move |this, _, _, cx| {
242                this.selected_page = page;
243                cx.notify();
244            }))
245    }
246
247    fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
248        match self.selected_page {
249            SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
250            SelectedPage::Editing => {
251                crate::editing_page::render_editing_page(window, cx).into_any_element()
252            }
253            SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
254        }
255    }
256
257    fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
258        let theme_mode = read_theme_selection(cx);
259
260        v_flex().child(
261            h_flex().justify_between().child(Label::new("Theme")).child(
262                ToggleButtonGroup::single_row(
263                    "theme-selector-onboarding",
264                    [
265                        ToggleButtonSimple::new("Light", |_, _, cx| {
266                            write_theme_selection(ThemeMode::Light, cx)
267                        }),
268                        ToggleButtonSimple::new("Dark", |_, _, cx| {
269                            write_theme_selection(ThemeMode::Dark, cx)
270                        }),
271                        ToggleButtonSimple::new("System", |_, _, cx| {
272                            write_theme_selection(ThemeMode::System, cx)
273                        }),
274                    ],
275                )
276                .selected_index(match theme_mode {
277                    ThemeMode::Light => 0,
278                    ThemeMode::Dark => 1,
279                    ThemeMode::System => 2,
280                })
281                .style(ui::ToggleButtonGroupStyle::Outlined)
282                .button_width(rems_from_px(64.)),
283            ),
284        )
285    }
286
287    fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
288        div().child("ai setup page")
289    }
290}
291
292impl Render for Onboarding {
293    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
294        h_flex()
295            .image_cache(gpui::retain_all("onboarding-page"))
296            .key_context("onboarding-page")
297            .px_24()
298            .py_12()
299            .items_start()
300            .child(
301                v_flex()
302                    .w_1_3()
303                    .h_full()
304                    .child(
305                        h_flex()
306                            .pt_0p5()
307                            .child(Vector::square(VectorName::ZedLogo, rems(2.)))
308                            .child(
309                                v_flex()
310                                    .left_1()
311                                    .items_center()
312                                    .child(Headline::new("Welcome to Zed"))
313                                    .child(
314                                        Label::new("The editor for what's next")
315                                            .color(Color::Muted)
316                                            .italic(),
317                                    ),
318                            ),
319                    )
320                    .p_1()
321                    .child(Divider::horizontal_dashed())
322                    .child(
323                        v_flex().gap_1().children([
324                            self.render_page_nav(SelectedPage::Basics, window, cx)
325                                .into_element(),
326                            self.render_page_nav(SelectedPage::Editing, window, cx)
327                                .into_element(),
328                            self.render_page_nav(SelectedPage::AiSetup, window, cx)
329                                .into_element(),
330                        ]),
331                    ),
332            )
333            // .child(Divider::vertical_dashed())
334            .child(div().w_2_3().h_full().child(self.render_page(window, cx)))
335    }
336}
337
338impl EventEmitter<ItemEvent> for Onboarding {}
339
340impl Focusable for Onboarding {
341    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
342        self.focus_handle.clone()
343    }
344}
345
346impl Item for Onboarding {
347    type Event = ItemEvent;
348
349    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
350        "Onboarding".into()
351    }
352
353    fn telemetry_event_text(&self) -> Option<&'static str> {
354        Some("Onboarding Page Opened")
355    }
356
357    fn show_toolbar(&self) -> bool {
358        false
359    }
360
361    fn clone_on_split(
362        &self,
363        _workspace_id: Option<WorkspaceId>,
364        _: &mut Window,
365        cx: &mut Context<Self>,
366    ) -> Option<Entity<Self>> {
367        Some(Onboarding::new(self.workspace.clone(), cx))
368    }
369
370    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
371        f(*event)
372    }
373}