welcome.rs

  1use gpui::{
  2    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
  3    NoAction, ParentElement, Render, Styled, 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 a Repo",
 41                // TODO: use proper action
 42                action: &NoAction,
 43            },
 44            SectionEntry {
 45                icon: IconName::ListCollapse,
 46                title: "Open Command Palette",
 47                action: &command_palette::Toggle,
 48            },
 49        ],
 50    },
 51    Section {
 52        title: "Configure",
 53        entries: [
 54            SectionEntry {
 55                icon: IconName::Settings,
 56                title: "Open Settings",
 57                action: &OpenSettings,
 58            },
 59            SectionEntry {
 60                icon: IconName::ZedAssistant,
 61                title: "View AI Settings",
 62                action: &agent::OpenSettings,
 63            },
 64            SectionEntry {
 65                icon: IconName::Blocks,
 66                title: "Explore Extensions",
 67                action: &Extensions {
 68                    category_filter: None,
 69                    id: None,
 70                },
 71            },
 72        ],
 73    },
 74);
 75
 76struct Section<const COLS: usize> {
 77    title: &'static str,
 78    entries: [SectionEntry; COLS],
 79}
 80
 81impl<const COLS: usize> Section<COLS> {
 82    fn render(
 83        self,
 84        index_offset: usize,
 85        focus: &FocusHandle,
 86        window: &mut Window,
 87        cx: &mut App,
 88    ) -> impl IntoElement {
 89        v_flex()
 90            .min_w_full()
 91            .child(
 92                h_flex()
 93                    .px_1()
 94                    .mb_2()
 95                    .gap_2()
 96                    .child(
 97                        Label::new(self.title.to_ascii_uppercase())
 98                            .buffer_font(cx)
 99                            .color(Color::Muted)
100                            .size(LabelSize::XSmall),
101                    )
102                    .child(Divider::horizontal().color(DividerColor::BorderVariant)),
103            )
104            .children(
105                self.entries
106                    .iter()
107                    .enumerate()
108                    .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)),
109            )
110    }
111}
112
113struct SectionEntry {
114    icon: IconName,
115    title: &'static str,
116    action: &'static dyn Action,
117}
118
119impl SectionEntry {
120    fn render(
121        &self,
122        button_index: usize,
123        focus: &FocusHandle,
124        window: &Window,
125        cx: &App,
126    ) -> impl IntoElement {
127        ButtonLike::new(("onboarding-button-id", button_index))
128            .tab_index(button_index as isize)
129            .full_width()
130            .size(ButtonSize::Medium)
131            .child(
132                h_flex()
133                    .w_full()
134                    .justify_between()
135                    .child(
136                        h_flex()
137                            .gap_2()
138                            .child(
139                                Icon::new(self.icon)
140                                    .color(Color::Muted)
141                                    .size(IconSize::XSmall),
142                            )
143                            .child(Label::new(self.title)),
144                    )
145                    .children(
146                        KeyBinding::for_action_in(self.action, focus, window, cx)
147                            .map(|s| s.size(rems_from_px(12.))),
148                    ),
149            )
150            .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
151    }
152}
153
154pub struct WelcomePage {
155    focus_handle: FocusHandle,
156}
157
158impl WelcomePage {
159    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
160        window.focus_next();
161        cx.notify();
162    }
163
164    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
165        window.focus_prev();
166        cx.notify();
167    }
168}
169
170impl Render for WelcomePage {
171    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
172        let (first_section, second_section) = CONTENT;
173        let first_section_entries = first_section.entries.len();
174        let last_index = first_section_entries + second_section.entries.len();
175
176        h_flex()
177            .size_full()
178            .justify_center()
179            .overflow_hidden()
180            .bg(cx.theme().colors().editor_background)
181            .key_context("Welcome")
182            .track_focus(&self.focus_handle(cx))
183            .on_action(cx.listener(Self::select_previous))
184            .on_action(cx.listener(Self::select_next))
185            .child(
186                h_flex()
187                    .px_12()
188                    .py_40()
189                    .size_full()
190                    .relative()
191                    .max_w(px(1100.))
192                    .child(
193                        div()
194                            .size_full()
195                            .max_w_128()
196                            .mx_auto()
197                            .child(
198                                h_flex()
199                                    .w_full()
200                                    .justify_center()
201                                    .gap_4()
202                                    .child(Vector::square(VectorName::ZedLogo, rems(2.)))
203                                    .child(
204                                        div().child(Headline::new("Welcome to Zed")).child(
205                                            Label::new("The editor for what's next")
206                                                .size(LabelSize::Small)
207                                                .color(Color::Muted)
208                                                .italic(),
209                                        ),
210                                    ),
211                            )
212                            .child(
213                                v_flex()
214                                    .mt_10()
215                                    .gap_6()
216                                    .child(first_section.render(
217                                        Default::default(),
218                                        &self.focus_handle,
219                                        window,
220                                        cx,
221                                    ))
222                                    .child(second_section.render(
223                                        first_section_entries,
224                                        &self.focus_handle,
225                                        window,
226                                        cx,
227                                    ))
228                                    .child(
229                                        h_flex()
230                                            .w_full()
231                                            .pt_4()
232                                            .justify_center()
233                                            // We call this a hack
234                                            .rounded_b_xs()
235                                            .border_t_1()
236                                            .border_color(cx.theme().colors().border.opacity(0.6))
237                                            .border_dashed()
238                                            .child(
239                                                    Button::new("welcome-exit", "Return to Setup")
240                                                        .tab_index(last_index as isize)
241                                                        .full_width()
242                                                        .label_size(LabelSize::XSmall)
243                                                        .on_click(|_, window, cx| {
244                                                            window.dispatch_action(
245                                                                OpenOnboarding.boxed_clone(),
246                                                                cx,
247                                                            );
248
249                                                            with_active_or_new_workspace(cx, |workspace, window, cx| {
250                                                                let Some((welcome_id, welcome_idx)) = workspace
251                                                                    .active_pane()
252                                                                    .read(cx)
253                                                                    .items()
254                                                                    .enumerate()
255                                                                    .find_map(|(idx, item)| {
256                                                                        let _ = item.downcast::<WelcomePage>()?;
257                                                                        Some((item.item_id(), idx))
258                                                                    })
259                                                                else {
260                                                                    return;
261                                                                };
262
263                                                                workspace.active_pane().update(cx, |pane, cx| {
264                                                                    // Get the index here to get around the borrow checker
265                                                                    let idx = pane.items().enumerate().find_map(
266                                                                        |(idx, item)| {
267                                                                            let _ =
268                                                                                item.downcast::<Onboarding>()?;
269                                                                            Some(idx)
270                                                                        },
271                                                                    );
272
273                                                                    if let Some(idx) = idx {
274                                                                        pane.activate_item(
275                                                                            idx, true, true, window, cx,
276                                                                        );
277                                                                    } else {
278                                                                        let item =
279                                                                            Box::new(Onboarding::new(workspace, cx));
280                                                                        pane.add_item(
281                                                                            item,
282                                                                            true,
283                                                                            true,
284                                                                            Some(welcome_idx),
285                                                                            window,
286                                                                            cx,
287                                                                        );
288                                                                    }
289
290                                                                    pane.remove_item(
291                                                                        welcome_id,
292                                                                        false,
293                                                                        false,
294                                                                        window,
295                                                                        cx,
296                                                                    );
297                                                                });
298                                                            });
299                                                        }),
300                                                ),
301                                    ),
302                            ),
303                    ),
304            )
305    }
306}
307
308impl WelcomePage {
309    pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
310        cx.new(|cx| {
311            let focus_handle = cx.focus_handle();
312            cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
313                .detach();
314
315            WelcomePage { focus_handle }
316        })
317    }
318}
319
320impl EventEmitter<ItemEvent> for WelcomePage {}
321
322impl Focusable for WelcomePage {
323    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
324        self.focus_handle.clone()
325    }
326}
327
328impl Item for WelcomePage {
329    type Event = ItemEvent;
330
331    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
332        "Welcome".into()
333    }
334
335    fn telemetry_event_text(&self) -> Option<&'static str> {
336        Some("New Welcome Page Opened")
337    }
338
339    fn show_toolbar(&self) -> bool {
340        false
341    }
342
343    fn clone_on_split(
344        &self,
345        _workspace_id: Option<WorkspaceId>,
346        _: &mut Window,
347        _: &mut Context<Self>,
348    ) -> Option<Entity<Self>> {
349        None
350    }
351
352    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
353        f(*event)
354    }
355}