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, Workspace, WorkspaceId,
8 item::{Item, ItemEvent},
9};
10use zed_actions::{Extensions, OpenSettings, agent, command_palette};
11
12actions!(
13 zed,
14 [
15 /// Show the Zed welcome screen
16 ShowWelcome
17 ]
18);
19
20const CONTENT: (Section<4>, Section<3>) = (
21 Section {
22 title: "Get Started",
23 entries: [
24 SectionEntry {
25 icon: IconName::Plus,
26 title: "New File",
27 action: &NewFile,
28 },
29 SectionEntry {
30 icon: IconName::FolderOpen,
31 title: "Open Project",
32 action: &Open,
33 },
34 SectionEntry {
35 icon: IconName::CloudDownload,
36 title: "Clone a Repo",
37 // TODO: use proper action
38 action: &NoAction,
39 },
40 SectionEntry {
41 icon: IconName::ListCollapse,
42 title: "Open Command Palette",
43 action: &command_palette::Toggle,
44 },
45 ],
46 },
47 Section {
48 title: "Configure",
49 entries: [
50 SectionEntry {
51 icon: IconName::Settings,
52 title: "Open Settings",
53 action: &OpenSettings,
54 },
55 SectionEntry {
56 icon: IconName::ZedAssistant,
57 title: "View AI Settings",
58 action: &agent::OpenSettings,
59 },
60 SectionEntry {
61 icon: IconName::Blocks,
62 title: "Explore Extensions",
63 action: &Extensions {
64 category_filter: None,
65 id: None,
66 },
67 },
68 ],
69 },
70);
71
72struct Section<const COLS: usize> {
73 title: &'static str,
74 entries: [SectionEntry; COLS],
75}
76
77impl<const COLS: usize> Section<COLS> {
78 fn render(
79 self,
80 index_offset: usize,
81 focus: &FocusHandle,
82 window: &mut Window,
83 cx: &mut App,
84 ) -> impl IntoElement {
85 v_flex()
86 .min_w_full()
87 .gap_2()
88 .child(
89 h_flex()
90 .px_1()
91 .gap_4()
92 .child(
93 Label::new(self.title.to_ascii_uppercase())
94 .buffer_font(cx)
95 .color(Color::Muted)
96 .size(LabelSize::XSmall),
97 )
98 .child(Divider::horizontal().color(DividerColor::Border)),
99 )
100 .children(
101 self.entries
102 .iter()
103 .enumerate()
104 .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)),
105 )
106 }
107}
108
109struct SectionEntry {
110 icon: IconName,
111 title: &'static str,
112 action: &'static dyn Action,
113}
114
115impl SectionEntry {
116 fn render(
117 &self,
118 button_index: usize,
119 focus: &FocusHandle,
120 window: &Window,
121 cx: &App,
122 ) -> impl IntoElement {
123 ButtonLike::new(("onboarding-button-id", button_index))
124 .full_width()
125 .child(
126 h_flex()
127 .w_full()
128 .gap_1()
129 .justify_between()
130 .child(
131 h_flex()
132 .gap_2()
133 .child(
134 Icon::new(self.icon)
135 .color(Color::Muted)
136 .size(IconSize::XSmall),
137 )
138 .child(Label::new(self.title)),
139 )
140 .children(KeyBinding::for_action_in(self.action, focus, window, cx)),
141 )
142 .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
143 }
144}
145
146pub struct WelcomePage {
147 focus_handle: FocusHandle,
148}
149
150impl Render for WelcomePage {
151 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
152 let (first_section, second_entries) = CONTENT;
153 let first_section_entries = first_section.entries.len();
154
155 h_flex()
156 .size_full()
157 .justify_center()
158 .overflow_hidden()
159 .bg(cx.theme().colors().editor_background)
160 .key_context("Welcome")
161 .track_focus(&self.focus_handle(cx))
162 .child(
163 h_flex()
164 .px_12()
165 .py_40()
166 .size_full()
167 .relative()
168 .max_w(px(1100.))
169 .child(
170 div()
171 .size_full()
172 .max_w_128()
173 .mx_auto()
174 .child(
175 h_flex()
176 .w_full()
177 .justify_center()
178 .gap_4()
179 .child(Vector::square(VectorName::ZedLogo, rems(2.)))
180 .child(
181 div().child(Headline::new("Welcome to Zed")).child(
182 Label::new("The editor for what's next")
183 .size(LabelSize::Small)
184 .color(Color::Muted)
185 .italic(),
186 ),
187 ),
188 )
189 .child(
190 v_flex()
191 .mt_12()
192 .gap_8()
193 .child(first_section.render(
194 Default::default(),
195 &self.focus_handle,
196 window,
197 cx,
198 ))
199 .child(second_entries.render(
200 first_section_entries,
201 &self.focus_handle,
202 window,
203 cx,
204 ))
205 .child(
206 h_flex()
207 .w_full()
208 .pt_4()
209 .justify_center()
210 // We call this a hack
211 .rounded_b_xs()
212 .border_t_1()
213 .border_color(DividerColor::Border.hsla(cx))
214 .border_dashed()
215 .child(
216 div().child(
217 Button::new("welcome-exit", "Return to Setup")
218 .full_width()
219 .label_size(LabelSize::XSmall),
220 ),
221 ),
222 ),
223 ),
224 ),
225 )
226 }
227}
228
229impl WelcomePage {
230 pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
231 cx.new(|cx| {
232 let focus_handle = cx.focus_handle();
233 cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
234 .detach();
235
236 WelcomePage { focus_handle }
237 })
238 }
239}
240
241impl EventEmitter<ItemEvent> for WelcomePage {}
242
243impl Focusable for WelcomePage {
244 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
245 self.focus_handle.clone()
246 }
247}
248
249impl Item for WelcomePage {
250 type Event = ItemEvent;
251
252 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
253 "Welcome".into()
254 }
255
256 fn telemetry_event_text(&self) -> Option<&'static str> {
257 Some("New Welcome Page Opened")
258 }
259
260 fn show_toolbar(&self) -> bool {
261 false
262 }
263
264 fn clone_on_split(
265 &self,
266 _workspace_id: Option<WorkspaceId>,
267 _: &mut Window,
268 _: &mut Context<Self>,
269 ) -> Option<Entity<Self>> {
270 None
271 }
272
273 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
274 f(*event)
275 }
276}