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 .gap_2()
91 .child(
92 h_flex()
93 .px_1()
94 .gap_4()
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::Border)),
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 .child(
129 h_flex()
130 .w_full()
131 .gap_1()
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(KeyBinding::for_action_in(self.action, focus, window, cx)),
144 )
145 .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
146 }
147}
148
149pub struct WelcomePage {
150 focus_handle: FocusHandle,
151}
152
153impl Render for WelcomePage {
154 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
155 let (first_section, second_entries) = CONTENT;
156 let first_section_entries = first_section.entries.len();
157
158 h_flex()
159 .size_full()
160 .justify_center()
161 .overflow_hidden()
162 .bg(cx.theme().colors().editor_background)
163 .key_context("Welcome")
164 .track_focus(&self.focus_handle(cx))
165 .child(
166 h_flex()
167 .px_12()
168 .py_40()
169 .size_full()
170 .relative()
171 .max_w(px(1100.))
172 .child(
173 div()
174 .size_full()
175 .max_w_128()
176 .mx_auto()
177 .child(
178 h_flex()
179 .w_full()
180 .justify_center()
181 .gap_4()
182 .child(Vector::square(VectorName::ZedLogo, rems(2.)))
183 .child(
184 div().child(Headline::new("Welcome to Zed")).child(
185 Label::new("The editor for what's next")
186 .size(LabelSize::Small)
187 .color(Color::Muted)
188 .italic(),
189 ),
190 ),
191 )
192 .child(
193 v_flex()
194 .mt_12()
195 .gap_8()
196 .child(first_section.render(
197 Default::default(),
198 &self.focus_handle,
199 window,
200 cx,
201 ))
202 .child(second_entries.render(
203 first_section_entries,
204 &self.focus_handle,
205 window,
206 cx,
207 ))
208 .child(
209 h_flex()
210 .w_full()
211 .pt_4()
212 .justify_center()
213 // We call this a hack
214 .rounded_b_xs()
215 .border_t_1()
216 .border_color(DividerColor::Border.hsla(cx))
217 .border_dashed()
218 .child(
219 div().child(
220 Button::new("welcome-exit", "Return to Setup")
221 .full_width()
222 .label_size(LabelSize::XSmall)
223 .on_click(|_, window, cx| {
224 window.dispatch_action(
225 OpenOnboarding.boxed_clone(),
226 cx,
227 );
228
229 with_active_or_new_workspace(cx, |workspace, window, cx| {
230 let Some((welcome_id, welcome_idx)) = workspace
231 .active_pane()
232 .read(cx)
233 .items()
234 .enumerate()
235 .find_map(|(idx, item)| {
236 let _ = item.downcast::<WelcomePage>()?;
237 Some((item.item_id(), idx))
238 })
239 else {
240 return;
241 };
242
243 workspace.active_pane().update(cx, |pane, cx| {
244 // Get the index here to get around the borrow checker
245 let idx = pane.items().enumerate().find_map(
246 |(idx, item)| {
247 let _ =
248 item.downcast::<Onboarding>()?;
249 Some(idx)
250 },
251 );
252
253 if let Some(idx) = idx {
254 pane.activate_item(
255 idx, true, true, window, cx,
256 );
257 } else {
258 let item =
259 Box::new(Onboarding::new(workspace, cx));
260 pane.add_item(
261 item,
262 true,
263 true,
264 Some(welcome_idx),
265 window,
266 cx,
267 );
268 }
269
270 pane.remove_item(
271 welcome_id,
272 false,
273 false,
274 window,
275 cx,
276 );
277 });
278 });
279 }),
280 ),
281 ),
282 ),
283 ),
284 ),
285 )
286 }
287}
288
289impl WelcomePage {
290 pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
291 cx.new(|cx| {
292 let focus_handle = cx.focus_handle();
293 cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
294 .detach();
295
296 WelcomePage { focus_handle }
297 })
298 }
299}
300
301impl EventEmitter<ItemEvent> for WelcomePage {}
302
303impl Focusable for WelcomePage {
304 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
305 self.focus_handle.clone()
306 }
307}
308
309impl Item for WelcomePage {
310 type Event = ItemEvent;
311
312 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
313 "Welcome".into()
314 }
315
316 fn telemetry_event_text(&self) -> Option<&'static str> {
317 Some("New Welcome Page Opened")
318 }
319
320 fn show_toolbar(&self) -> bool {
321 false
322 }
323
324 fn clone_on_split(
325 &self,
326 _workspace_id: Option<WorkspaceId>,
327 _: &mut Window,
328 _: &mut Context<Self>,
329 ) -> Option<Entity<Self>> {
330 None
331 }
332
333 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
334 f(*event)
335 }
336}