welcome.rs
1use gpui::{
2 Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
3 NoAction, ParentElement, Render, Styled, Window, actions,
4};
5use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
6use workspace::{
7 NewFile, Open, WorkspaceId,
8 item::{Item, ItemEvent},
9 with_active_or_new_workspace,
10};
11use zed_actions::{Extensions, OpenSettings, agent, command_palette};
12
13use crate::{Onboarding, OpenOnboarding};
14
15actions!(
16 zed,
17 [
18 /// Show the Zed welcome screen
19 ShowWelcome
20 ]
21);
22
23const CONTENT: (Section<4>, Section<3>) = (
24 Section {
25 title: "Get Started",
26 entries: [
27 SectionEntry {
28 icon: IconName::Plus,
29 title: "New File",
30 action: &NewFile,
31 },
32 SectionEntry {
33 icon: IconName::FolderOpen,
34 title: "Open Project",
35 action: &Open,
36 },
37 SectionEntry {
38 icon: IconName::CloudDownload,
39 title: "Clone a Repo",
40 // TODO: use proper action
41 action: &NoAction,
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 .full_width()
128 .size(ButtonSize::Medium)
129 .child(
130 h_flex()
131 .w_full()
132 .justify_between()
133 .child(
134 h_flex()
135 .gap_2()
136 .child(
137 Icon::new(self.icon)
138 .color(Color::Muted)
139 .size(IconSize::XSmall),
140 )
141 .child(Label::new(self.title)),
142 )
143 .children(
144 KeyBinding::for_action_in(self.action, focus, window, cx)
145 .map(|s| s.size(rems_from_px(12.))),
146 ),
147 )
148 .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
149 }
150}
151
152pub struct WelcomePage {
153 focus_handle: FocusHandle,
154}
155
156impl Render for WelcomePage {
157 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
158 let (first_section, second_entries) = CONTENT;
159 let first_section_entries = first_section.entries.len();
160
161 h_flex()
162 .size_full()
163 .justify_center()
164 .overflow_hidden()
165 .bg(cx.theme().colors().editor_background)
166 .key_context("Welcome")
167 .track_focus(&self.focus_handle(cx))
168 .child(
169 h_flex()
170 .px_12()
171 .py_40()
172 .size_full()
173 .relative()
174 .max_w(px(1100.))
175 .child(
176 div()
177 .size_full()
178 .max_w_128()
179 .mx_auto()
180 .child(
181 h_flex()
182 .w_full()
183 .justify_center()
184 .gap_4()
185 .child(Vector::square(VectorName::ZedLogo, rems(2.)))
186 .child(
187 div().child(Headline::new("Welcome to Zed")).child(
188 Label::new("The editor for what's next")
189 .size(LabelSize::Small)
190 .color(Color::Muted)
191 .italic(),
192 ),
193 ),
194 )
195 .child(
196 v_flex()
197 .mt_10()
198 .gap_6()
199 .child(first_section.render(
200 Default::default(),
201 &self.focus_handle,
202 window,
203 cx,
204 ))
205 .child(second_entries.render(
206 first_section_entries,
207 &self.focus_handle,
208 window,
209 cx,
210 ))
211 .child(
212 h_flex()
213 .w_full()
214 .pt_4()
215 .justify_center()
216 // We call this a hack
217 .rounded_b_xs()
218 .border_t_1()
219 .border_color(cx.theme().colors().border.opacity(0.6))
220 .border_dashed()
221 .child(
222 Button::new("welcome-exit", "Return to Setup")
223 .full_width()
224 .label_size(LabelSize::XSmall)
225 .on_click(|_, window, cx| {
226 window.dispatch_action(
227 OpenOnboarding.boxed_clone(),
228 cx,
229 );
230
231 with_active_or_new_workspace(cx, |workspace, window, cx| {
232 let Some((welcome_id, welcome_idx)) = workspace
233 .active_pane()
234 .read(cx)
235 .items()
236 .enumerate()
237 .find_map(|(idx, item)| {
238 let _ = item.downcast::<WelcomePage>()?;
239 Some((item.item_id(), idx))
240 })
241 else {
242 return;
243 };
244
245 workspace.active_pane().update(cx, |pane, cx| {
246 // Get the index here to get around the borrow checker
247 let idx = pane.items().enumerate().find_map(
248 |(idx, item)| {
249 let _ =
250 item.downcast::<Onboarding>()?;
251 Some(idx)
252 },
253 );
254
255 if let Some(idx) = idx {
256 pane.activate_item(
257 idx, true, true, window, cx,
258 );
259 } else {
260 let item =
261 Box::new(Onboarding::new(workspace, cx));
262 pane.add_item(
263 item,
264 true,
265 true,
266 Some(welcome_idx),
267 window,
268 cx,
269 );
270 }
271
272 pane.remove_item(
273 welcome_id,
274 false,
275 false,
276 window,
277 cx,
278 );
279 });
280 });
281 }),
282 ),
283 ),
284 ),
285 ),
286 )
287 }
288}
289
290impl WelcomePage {
291 pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
292 cx.new(|cx| {
293 let focus_handle = cx.focus_handle();
294 cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
295 .detach();
296
297 WelcomePage { focus_handle }
298 })
299 }
300}
301
302impl EventEmitter<ItemEvent> for WelcomePage {}
303
304impl Focusable for WelcomePage {
305 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
306 self.focus_handle.clone()
307 }
308}
309
310impl Item for WelcomePage {
311 type Event = ItemEvent;
312
313 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
314 "Welcome".into()
315 }
316
317 fn telemetry_event_text(&self) -> Option<&'static str> {
318 Some("New Welcome Page Opened")
319 }
320
321 fn show_toolbar(&self) -> bool {
322 false
323 }
324
325 fn clone_on_split(
326 &self,
327 _workspace_id: Option<WorkspaceId>,
328 _: &mut Window,
329 _: &mut Context<Self>,
330 ) -> Option<Entity<Self>> {
331 None
332 }
333
334 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
335 f(*event)
336 }
337}