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}