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}