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