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