onboarding.rs

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