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}