welcome.rs

  1use gpui::{
  2    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
  3    ParentElement, Render, Styled, Task, Window, actions,
  4};
  5use menu::{SelectNext, SelectPrevious};
  6use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
  7use workspace::{
  8    NewFile, Open, WorkspaceId,
  9    item::{Item, ItemEvent},
 10    with_active_or_new_workspace,
 11};
 12use zed_actions::{Extensions, OpenSettings, agent, command_palette};
 13
 14use crate::{Onboarding, OpenOnboarding};
 15
 16actions!(
 17    zed,
 18    [
 19        /// Show the Zed welcome screen
 20        ShowWelcome
 21    ]
 22);
 23
 24const CONTENT: (Section<4>, Section<3>) = (
 25    Section {
 26        title: "Get Started",
 27        entries: [
 28            SectionEntry {
 29                icon: IconName::Plus,
 30                title: "New File",
 31                action: &NewFile,
 32            },
 33            SectionEntry {
 34                icon: IconName::FolderOpen,
 35                title: "Open Project",
 36                action: &Open,
 37            },
 38            SectionEntry {
 39                icon: IconName::CloudDownload,
 40                title: "Clone Repository",
 41                action: &git::Clone,
 42            },
 43            SectionEntry {
 44                icon: IconName::ListCollapse,
 45                title: "Open Command Palette",
 46                action: &command_palette::Toggle,
 47            },
 48        ],
 49    },
 50    Section {
 51        title: "Configure",
 52        entries: [
 53            SectionEntry {
 54                icon: IconName::Settings,
 55                title: "Open Settings",
 56                action: &OpenSettings,
 57            },
 58            SectionEntry {
 59                icon: IconName::ZedAssistant,
 60                title: "View AI Settings",
 61                action: &agent::OpenSettings,
 62            },
 63            SectionEntry {
 64                icon: IconName::Blocks,
 65                title: "Explore Extensions",
 66                action: &Extensions {
 67                    category_filter: None,
 68                    id: None,
 69                },
 70            },
 71        ],
 72    },
 73);
 74
 75struct Section<const COLS: usize> {
 76    title: &'static str,
 77    entries: [SectionEntry; COLS],
 78}
 79
 80impl<const COLS: usize> Section<COLS> {
 81    fn render(
 82        self,
 83        index_offset: usize,
 84        focus: &FocusHandle,
 85        window: &mut Window,
 86        cx: &mut App,
 87    ) -> impl IntoElement {
 88        v_flex()
 89            .min_w_full()
 90            .child(
 91                h_flex()
 92                    .px_1()
 93                    .mb_2()
 94                    .gap_2()
 95                    .child(
 96                        Label::new(self.title.to_ascii_uppercase())
 97                            .buffer_font(cx)
 98                            .color(Color::Muted)
 99                            .size(LabelSize::XSmall),
100                    )
101                    .child(Divider::horizontal().color(DividerColor::BorderVariant)),
102            )
103            .children(
104                self.entries
105                    .iter()
106                    .enumerate()
107                    .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)),
108            )
109    }
110}
111
112struct SectionEntry {
113    icon: IconName,
114    title: &'static str,
115    action: &'static dyn Action,
116}
117
118impl SectionEntry {
119    fn render(
120        &self,
121        button_index: usize,
122        focus: &FocusHandle,
123        window: &Window,
124        cx: &App,
125    ) -> impl IntoElement {
126        ButtonLike::new(("onboarding-button-id", button_index))
127            .tab_index(button_index as isize)
128            .full_width()
129            .size(ButtonSize::Medium)
130            .child(
131                h_flex()
132                    .w_full()
133                    .justify_between()
134                    .child(
135                        h_flex()
136                            .gap_2()
137                            .child(
138                                Icon::new(self.icon)
139                                    .color(Color::Muted)
140                                    .size(IconSize::XSmall),
141                            )
142                            .child(Label::new(self.title)),
143                    )
144                    .children(
145                        KeyBinding::for_action_in(self.action, focus, window, cx)
146                            .map(|s| s.size(rems_from_px(12.))),
147                    ),
148            )
149            .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
150    }
151}
152
153pub struct WelcomePage {
154    focus_handle: FocusHandle,
155}
156
157impl WelcomePage {
158    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
159        window.focus_next();
160        cx.notify();
161    }
162
163    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
164        window.focus_prev();
165        cx.notify();
166    }
167}
168
169impl Render for WelcomePage {
170    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
171        let (first_section, second_section) = CONTENT;
172        let first_section_entries = first_section.entries.len();
173        let last_index = first_section_entries + second_section.entries.len();
174
175        h_flex()
176            .size_full()
177            .justify_center()
178            .overflow_hidden()
179            .bg(cx.theme().colors().editor_background)
180            .key_context("Welcome")
181            .track_focus(&self.focus_handle(cx))
182            .on_action(cx.listener(Self::select_previous))
183            .on_action(cx.listener(Self::select_next))
184            .child(
185                h_flex()
186                    .px_12()
187                    .py_40()
188                    .size_full()
189                    .relative()
190                    .max_w(px(1100.))
191                    .child(
192                        div()
193                            .size_full()
194                            .max_w_128()
195                            .mx_auto()
196                            .child(
197                                h_flex()
198                                    .w_full()
199                                    .justify_center()
200                                    .gap_4()
201                                    .child(Vector::square(VectorName::ZedLogo, rems(2.)))
202                                    .child(
203                                        div().child(Headline::new("Welcome to Zed")).child(
204                                            Label::new("The editor for what's next")
205                                                .size(LabelSize::Small)
206                                                .color(Color::Muted)
207                                                .italic(),
208                                        ),
209                                    ),
210                            )
211                            .child(
212                                v_flex()
213                                    .mt_10()
214                                    .gap_6()
215                                    .child(first_section.render(
216                                        Default::default(),
217                                        &self.focus_handle,
218                                        window,
219                                        cx,
220                                    ))
221                                    .child(second_section.render(
222                                        first_section_entries,
223                                        &self.focus_handle,
224                                        window,
225                                        cx,
226                                    ))
227                                    .child(
228                                        h_flex()
229                                            .w_full()
230                                            .pt_4()
231                                            .justify_center()
232                                            // We call this a hack
233                                            .rounded_b_xs()
234                                            .border_t_1()
235                                            .border_color(cx.theme().colors().border.opacity(0.6))
236                                            .border_dashed()
237                                            .child(
238                                                    Button::new("welcome-exit", "Return to Setup")
239                                                        .tab_index(last_index as isize)
240                                                        .full_width()
241                                                        .label_size(LabelSize::XSmall)
242                                                        .on_click(|_, window, cx| {
243                                                            window.dispatch_action(
244                                                                OpenOnboarding.boxed_clone(),
245                                                                cx,
246                                                            );
247
248                                                            with_active_or_new_workspace(cx, |workspace, window, cx| {
249                                                                let Some((welcome_id, welcome_idx)) = workspace
250                                                                    .active_pane()
251                                                                    .read(cx)
252                                                                    .items()
253                                                                    .enumerate()
254                                                                    .find_map(|(idx, item)| {
255                                                                        let _ = item.downcast::<WelcomePage>()?;
256                                                                        Some((item.item_id(), idx))
257                                                                    })
258                                                                else {
259                                                                    return;
260                                                                };
261
262                                                                workspace.active_pane().update(cx, |pane, cx| {
263                                                                    // Get the index here to get around the borrow checker
264                                                                    let idx = pane.items().enumerate().find_map(
265                                                                        |(idx, item)| {
266                                                                            let _ =
267                                                                                item.downcast::<Onboarding>()?;
268                                                                            Some(idx)
269                                                                        },
270                                                                    );
271
272                                                                    if let Some(idx) = idx {
273                                                                        pane.activate_item(
274                                                                            idx, true, true, window, cx,
275                                                                        );
276                                                                    } else {
277                                                                        let item =
278                                                                            Box::new(Onboarding::new(workspace, cx));
279                                                                        pane.add_item(
280                                                                            item,
281                                                                            true,
282                                                                            true,
283                                                                            Some(welcome_idx),
284                                                                            window,
285                                                                            cx,
286                                                                        );
287                                                                    }
288
289                                                                    pane.remove_item(
290                                                                        welcome_id,
291                                                                        false,
292                                                                        false,
293                                                                        window,
294                                                                        cx,
295                                                                    );
296                                                                });
297                                                            });
298                                                        }),
299                                                ),
300                                    ),
301                            ),
302                    ),
303            )
304    }
305}
306
307impl WelcomePage {
308    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
309        cx.new(|cx| {
310            let focus_handle = cx.focus_handle();
311            cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
312                .detach();
313
314            WelcomePage { focus_handle }
315        })
316    }
317}
318
319impl EventEmitter<ItemEvent> for WelcomePage {}
320
321impl Focusable for WelcomePage {
322    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
323        self.focus_handle.clone()
324    }
325}
326
327impl Item for WelcomePage {
328    type Event = ItemEvent;
329
330    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
331        "Welcome".into()
332    }
333
334    fn telemetry_event_text(&self) -> Option<&'static str> {
335        Some("New Welcome Page Opened")
336    }
337
338    fn show_toolbar(&self) -> bool {
339        false
340    }
341
342    fn clone_on_split(
343        &self,
344        _workspace_id: Option<WorkspaceId>,
345        _: &mut Window,
346        _: &mut Context<Self>,
347    ) -> Option<Entity<Self>> {
348        None
349    }
350
351    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
352        f(*event)
353    }
354}
355
356impl workspace::SerializableItem for WelcomePage {
357    fn serialized_item_kind() -> &'static str {
358        "WelcomePage"
359    }
360
361    fn cleanup(
362        workspace_id: workspace::WorkspaceId,
363        alive_items: Vec<workspace::ItemId>,
364        _window: &mut Window,
365        cx: &mut App,
366    ) -> Task<gpui::Result<()>> {
367        workspace::delete_unloaded_items(
368            alive_items,
369            workspace_id,
370            "welcome_pages",
371            &persistence::WELCOME_PAGES,
372            cx,
373        )
374    }
375
376    fn deserialize(
377        _project: Entity<project::Project>,
378        _workspace: gpui::WeakEntity<workspace::Workspace>,
379        workspace_id: workspace::WorkspaceId,
380        item_id: workspace::ItemId,
381        window: &mut Window,
382        cx: &mut App,
383    ) -> Task<gpui::Result<Entity<Self>>> {
384        if persistence::WELCOME_PAGES
385            .get_welcome_page(item_id, workspace_id)
386            .ok()
387            .is_some_and(|is_open| is_open)
388        {
389            window.spawn(cx, async move |cx| cx.update(WelcomePage::new))
390        } else {
391            Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize")))
392        }
393    }
394
395    fn serialize(
396        &mut self,
397        workspace: &mut workspace::Workspace,
398        item_id: workspace::ItemId,
399        _closing: bool,
400        _window: &mut Window,
401        cx: &mut Context<Self>,
402    ) -> Option<Task<gpui::Result<()>>> {
403        let workspace_id = workspace.database_id()?;
404        Some(cx.background_spawn(async move {
405            persistence::WELCOME_PAGES
406                .save_welcome_page(item_id, workspace_id, true)
407                .await
408        }))
409    }
410
411    fn should_serialize(&self, event: &Self::Event) -> bool {
412        event == &ItemEvent::UpdateTab
413    }
414}
415
416mod persistence {
417    use db::{
418        query,
419        sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
420        sqlez_macros::sql,
421    };
422    use workspace::WorkspaceDb;
423
424    pub struct WelcomePagesDb(ThreadSafeConnection);
425
426    impl Domain for WelcomePagesDb {
427        const NAME: &str = stringify!(WelcomePagesDb);
428
429        const MIGRATIONS: &[&str] = (&[sql!(
430                    CREATE TABLE welcome_pages (
431                        workspace_id INTEGER,
432                        item_id INTEGER UNIQUE,
433                        is_open INTEGER DEFAULT FALSE,
434
435                        PRIMARY KEY(workspace_id, item_id),
436                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
437                        ON DELETE CASCADE
438                    ) STRICT;
439        )]);
440    }
441
442    db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]);
443
444    impl WelcomePagesDb {
445        query! {
446            pub async fn save_welcome_page(
447                item_id: workspace::ItemId,
448                workspace_id: workspace::WorkspaceId,
449                is_open: bool
450            ) -> Result<()> {
451                INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open)
452                VALUES (?, ?, ?)
453            }
454        }
455
456        query! {
457            pub fn get_welcome_page(
458                item_id: workspace::ItemId,
459                workspace_id: workspace::WorkspaceId
460            ) -> Result<bool> {
461                SELECT is_open
462                FROM welcome_pages
463                WHERE item_id = ? AND workspace_id = ?
464            }
465        }
466    }
467}