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}