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}