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