1use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
2use serde::{Deserialize, Serialize};
3use strum::EnumIter;
4
5use crate::{prelude::*, Indicator};
6
7#[derive(IntoElement)]
8pub enum AnyIcon {
9 Icon(Icon),
10 AnimatedIcon(AnimationElement<Icon>),
11}
12
13impl AnyIcon {
14 /// Returns a new [`AnyIcon`] after applying the given mapping function
15 /// to the contained [`Icon`].
16 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
17 match self {
18 Self::Icon(icon) => Self::Icon(f(icon)),
19 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
20 }
21 }
22}
23
24impl From<Icon> for AnyIcon {
25 fn from(value: Icon) -> Self {
26 Self::Icon(value)
27 }
28}
29
30impl From<AnimationElement<Icon>> for AnyIcon {
31 fn from(value: AnimationElement<Icon>) -> Self {
32 Self::AnimatedIcon(value)
33 }
34}
35
36impl RenderOnce for AnyIcon {
37 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
38 match self {
39 Self::Icon(icon) => icon.into_any_element(),
40 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
41 }
42 }
43}
44
45/// The decoration for an icon.
46///
47/// For example, this can show an indicator, an "x",
48/// or a diagonal strkethrough to indicate something is disabled.
49#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
50pub enum IconDecoration {
51 Strikethrough,
52 IndicatorDot,
53 X,
54}
55
56#[derive(Default, PartialEq, Copy, Clone)]
57pub enum IconSize {
58 /// 10px
59 Indicator,
60 /// 12px
61 XSmall,
62 /// 14px
63 Small,
64 #[default]
65 /// 16px
66 Medium,
67}
68
69impl IconSize {
70 pub fn rems(self) -> Rems {
71 match self {
72 IconSize::Indicator => rems_from_px(10.),
73 IconSize::XSmall => rems_from_px(12.),
74 IconSize::Small => rems_from_px(14.),
75 IconSize::Medium => rems_from_px(16.),
76 }
77 }
78}
79
80#[derive(Debug, PartialEq, Copy, Clone, EnumIter, Serialize, Deserialize)]
81pub enum IconName {
82 Ai,
83 ArrowCircle,
84 ArrowDown,
85 ArrowDownFromLine,
86 ArrowLeft,
87 ArrowRight,
88 ArrowUp,
89 ArrowUpFromLine,
90 ArrowUpRight,
91 AtSign,
92 AudioOff,
93 AudioOn,
94 Backspace,
95 Bell,
96 BellDot,
97 BellOff,
98 BellRing,
99 Bolt,
100 Book,
101 BookCopy,
102 BookPlus,
103 CaseSensitive,
104 Check,
105 ChevronDown,
106 ChevronLeft,
107 ChevronRight,
108 ChevronUp,
109 ChevronUpDown,
110 Close,
111 Code,
112 Collab,
113 Command,
114 Context,
115 Control,
116 Copilot,
117 CopilotDisabled,
118 CopilotError,
119 CopilotInit,
120 Copy,
121 CountdownTimer,
122 Dash,
123 Delete,
124 Disconnected,
125 Download,
126 Ellipsis,
127 Envelope,
128 Escape,
129 ExclamationTriangle,
130 Exit,
131 ExpandVertical,
132 ExternalLink,
133 File,
134 FileDoc,
135 FileGeneric,
136 FileGit,
137 FileLock,
138 FileRust,
139 FileToml,
140 FileTree,
141 Filter,
142 Folder,
143 FolderOpen,
144 FolderX,
145 Font,
146 FontSize,
147 FontWeight,
148 Github,
149 Hash,
150 HistoryRerun,
151 Indicator,
152 IndicatorX,
153 InlayHint,
154 Library,
155 LineHeight,
156 Link,
157 ListTree,
158 MagicWand,
159 MagnifyingGlass,
160 MailOpen,
161 Maximize,
162 Menu,
163 MessageBubbles,
164 Mic,
165 MicMute,
166 Minimize,
167 Option,
168 PageDown,
169 PageUp,
170 Pencil,
171 Person,
172 Play,
173 Plus,
174 Public,
175 PullRequest,
176 Quote,
177 Regex,
178 Replace,
179 ReplaceAll,
180 ReplaceNext,
181 ReplyArrowRight,
182 Rerun,
183 Return,
184 Reveal,
185 RotateCcw,
186 RotateCw,
187 Save,
188 Screen,
189 SearchSelection,
190 SelectAll,
191 Server,
192 Settings,
193 Shift,
194 Sliders,
195 Snip,
196 Space,
197 Sparkle,
198 SparkleFilled,
199 Spinner,
200 Split,
201 Star,
202 StarFilled,
203 Stop,
204 Strikethrough,
205 Supermaven,
206 SupermavenDisabled,
207 SupermavenError,
208 SupermavenInit,
209 Tab,
210 Terminal,
211 TextCursor,
212 Trash,
213 TriangleRight,
214 Update,
215 WholeWord,
216 XCircle,
217 ZedAssistant,
218 ZedAssistantFilled,
219 ZedXCopilot,
220 Visible,
221}
222
223impl IconName {
224 pub fn path(self) -> &'static str {
225 match self {
226 IconName::Ai => "icons/ai.svg",
227 IconName::ArrowCircle => "icons/arrow_circle.svg",
228 IconName::ArrowDown => "icons/arrow_down.svg",
229 IconName::ArrowDownFromLine => "icons/arrow_down_from_line.svg",
230 IconName::ArrowLeft => "icons/arrow_left.svg",
231 IconName::ArrowRight => "icons/arrow_right.svg",
232 IconName::ArrowUp => "icons/arrow_up.svg",
233 IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
234 IconName::ArrowUpRight => "icons/arrow_up_right.svg",
235 IconName::AtSign => "icons/at_sign.svg",
236 IconName::AudioOff => "icons/speaker_off.svg",
237 IconName::AudioOn => "icons/speaker_loud.svg",
238 IconName::Backspace => "icons/backspace.svg",
239 IconName::Bell => "icons/bell.svg",
240 IconName::BellDot => "icons/bell_dot.svg",
241 IconName::BellOff => "icons/bell_off.svg",
242 IconName::BellRing => "icons/bell_ring.svg",
243 IconName::Bolt => "icons/bolt.svg",
244 IconName::Book => "icons/book.svg",
245 IconName::BookCopy => "icons/book_copy.svg",
246 IconName::BookPlus => "icons/book_plus.svg",
247 IconName::CaseSensitive => "icons/case_insensitive.svg",
248 IconName::Check => "icons/check.svg",
249 IconName::ChevronDown => "icons/chevron_down.svg",
250 IconName::ChevronLeft => "icons/chevron_left.svg",
251 IconName::ChevronRight => "icons/chevron_right.svg",
252 IconName::ChevronUp => "icons/chevron_up.svg",
253 IconName::ChevronUpDown => "icons/chevron_up_down.svg",
254 IconName::Close => "icons/x.svg",
255 IconName::Code => "icons/code.svg",
256 IconName::Collab => "icons/user_group_16.svg",
257 IconName::Command => "icons/command.svg",
258 IconName::Context => "icons/context.svg",
259 IconName::Control => "icons/control.svg",
260 IconName::Copilot => "icons/copilot.svg",
261 IconName::CopilotDisabled => "icons/copilot_disabled.svg",
262 IconName::CopilotError => "icons/copilot_error.svg",
263 IconName::CopilotInit => "icons/copilot_init.svg",
264 IconName::Copy => "icons/copy.svg",
265 IconName::CountdownTimer => "icons/countdown_timer.svg",
266 IconName::Dash => "icons/dash.svg",
267 IconName::Delete => "icons/delete.svg",
268 IconName::Disconnected => "icons/disconnected.svg",
269 IconName::Download => "icons/download.svg",
270 IconName::Ellipsis => "icons/ellipsis.svg",
271 IconName::Envelope => "icons/feedback.svg",
272 IconName::Escape => "icons/escape.svg",
273 IconName::ExclamationTriangle => "icons/warning.svg",
274 IconName::Exit => "icons/exit.svg",
275 IconName::ExpandVertical => "icons/expand_vertical.svg",
276 IconName::ExternalLink => "icons/external_link.svg",
277 IconName::File => "icons/file.svg",
278 IconName::FileDoc => "icons/file_icons/book.svg",
279 IconName::FileGeneric => "icons/file_icons/file.svg",
280 IconName::FileGit => "icons/file_icons/git.svg",
281 IconName::FileLock => "icons/file_icons/lock.svg",
282 IconName::FileRust => "icons/file_icons/rust.svg",
283 IconName::FileToml => "icons/file_icons/toml.svg",
284 IconName::FileTree => "icons/project.svg",
285 IconName::Filter => "icons/filter.svg",
286 IconName::Folder => "icons/file_icons/folder.svg",
287 IconName::FolderOpen => "icons/file_icons/folder_open.svg",
288 IconName::FolderX => "icons/stop_sharing.svg",
289 IconName::Font => "icons/font.svg",
290 IconName::FontSize => "icons/font_size.svg",
291 IconName::FontWeight => "icons/font_weight.svg",
292 IconName::Github => "icons/github.svg",
293 IconName::Hash => "icons/hash.svg",
294 IconName::HistoryRerun => "icons/history_rerun.svg",
295 IconName::Indicator => "icons/indicator.svg",
296 IconName::IndicatorX => "icons/indicator_x.svg",
297 IconName::InlayHint => "icons/inlay_hint.svg",
298 IconName::Library => "icons/library.svg",
299 IconName::LineHeight => "icons/line_height.svg",
300 IconName::Link => "icons/link.svg",
301 IconName::ListTree => "icons/list_tree.svg",
302 IconName::MagicWand => "icons/magic_wand.svg",
303 IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
304 IconName::MailOpen => "icons/mail_open.svg",
305 IconName::Maximize => "icons/maximize.svg",
306 IconName::Menu => "icons/menu.svg",
307 IconName::MessageBubbles => "icons/conversations.svg",
308 IconName::Mic => "icons/mic.svg",
309 IconName::MicMute => "icons/mic_mute.svg",
310 IconName::Minimize => "icons/minimize.svg",
311 IconName::Option => "icons/option.svg",
312 IconName::PageDown => "icons/page_down.svg",
313 IconName::PageUp => "icons/page_up.svg",
314 IconName::Pencil => "icons/pencil.svg",
315 IconName::Person => "icons/person.svg",
316 IconName::Play => "icons/play.svg",
317 IconName::Plus => "icons/plus.svg",
318 IconName::Public => "icons/public.svg",
319 IconName::PullRequest => "icons/pull_request.svg",
320 IconName::Quote => "icons/quote.svg",
321 IconName::Regex => "icons/regex.svg",
322 IconName::Replace => "icons/replace.svg",
323 IconName::ReplaceAll => "icons/replace_all.svg",
324 IconName::ReplaceNext => "icons/replace_next.svg",
325 IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
326 IconName::Rerun => "icons/rerun.svg",
327 IconName::Return => "icons/return.svg",
328 IconName::Reveal => "icons/reveal.svg",
329 IconName::RotateCcw => "icons/rotate_ccw.svg",
330 IconName::RotateCw => "icons/rotate_cw.svg",
331 IconName::Save => "icons/save.svg",
332 IconName::Screen => "icons/desktop.svg",
333 IconName::SearchSelection => "icons/search_selection.svg",
334 IconName::SelectAll => "icons/select_all.svg",
335 IconName::Server => "icons/server.svg",
336 IconName::Settings => "icons/file_icons/settings.svg",
337 IconName::Shift => "icons/shift.svg",
338 IconName::Sliders => "icons/sliders.svg",
339 IconName::Snip => "icons/snip.svg",
340 IconName::Space => "icons/space.svg",
341 IconName::Sparkle => "icons/sparkle.svg",
342 IconName::SparkleFilled => "icons/sparkle_filled.svg",
343 IconName::Spinner => "icons/spinner.svg",
344 IconName::Split => "icons/split.svg",
345 IconName::Star => "icons/star.svg",
346 IconName::StarFilled => "icons/star_filled.svg",
347 IconName::Stop => "icons/stop.svg",
348 IconName::Strikethrough => "icons/strikethrough.svg",
349 IconName::Supermaven => "icons/supermaven.svg",
350 IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",
351 IconName::SupermavenError => "icons/supermaven_error.svg",
352 IconName::SupermavenInit => "icons/supermaven_init.svg",
353 IconName::Tab => "icons/tab.svg",
354 IconName::Terminal => "icons/terminal.svg",
355 IconName::TextCursor => "icons/text-cursor.svg",
356 IconName::Trash => "icons/trash.svg",
357 IconName::TriangleRight => "icons/triangle_right.svg",
358 IconName::Update => "icons/update.svg",
359 IconName::WholeWord => "icons/word_search.svg",
360 IconName::XCircle => "icons/error.svg",
361 IconName::ZedAssistant => "icons/zed_assistant.svg",
362 IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
363 IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
364 IconName::Visible => "icons/visible.svg",
365 }
366 }
367}
368
369#[derive(IntoElement)]
370pub struct Icon {
371 path: SharedString,
372 color: Color,
373 size: Rems,
374 transformation: Transformation,
375}
376
377impl Icon {
378 pub fn new(icon: IconName) -> Self {
379 Self {
380 path: icon.path().into(),
381 color: Color::default(),
382 size: IconSize::default().rems(),
383 transformation: Transformation::default(),
384 }
385 }
386
387 pub fn from_path(path: impl Into<SharedString>) -> Self {
388 Self {
389 path: path.into(),
390 color: Color::default(),
391 size: IconSize::default().rems(),
392 transformation: Transformation::default(),
393 }
394 }
395
396 pub fn color(mut self, color: Color) -> Self {
397 self.color = color;
398 self
399 }
400
401 pub fn size(mut self, size: IconSize) -> Self {
402 self.size = size.rems();
403 self
404 }
405
406 /// Sets a custom size for the icon, in [`Rems`].
407 ///
408 /// Not to be exposed outside of the `ui` crate.
409 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
410 self.size = size;
411 self
412 }
413
414 pub fn transform(mut self, transformation: Transformation) -> Self {
415 self.transformation = transformation;
416 self
417 }
418}
419
420impl RenderOnce for Icon {
421 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
422 svg()
423 .with_transformation(self.transformation)
424 .size(self.size)
425 .flex_none()
426 .path(self.path)
427 .text_color(self.color.color(cx))
428 }
429}
430
431#[derive(IntoElement)]
432pub struct DecoratedIcon {
433 icon: Icon,
434 decoration: IconDecoration,
435 decoration_color: Color,
436 parent_background: Option<Hsla>,
437}
438
439impl DecoratedIcon {
440 pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
441 Self {
442 icon,
443 decoration,
444 decoration_color: Color::Default,
445 parent_background: None,
446 }
447 }
448
449 pub fn decoration_color(mut self, color: Color) -> Self {
450 self.decoration_color = color;
451 self
452 }
453
454 pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
455 self.parent_background = background;
456 self
457 }
458}
459
460impl RenderOnce for DecoratedIcon {
461 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
462 let background = self
463 .parent_background
464 .unwrap_or(cx.theme().colors().background);
465
466 let size = self.icon.size;
467
468 let decoration_icon = match self.decoration {
469 IconDecoration::Strikethrough => IconName::Strikethrough,
470 IconDecoration::IndicatorDot => IconName::Indicator,
471 IconDecoration::X => IconName::IndicatorX,
472 };
473
474 let decoration_svg = |icon: IconName| {
475 svg()
476 .absolute()
477 .top_0()
478 .left_0()
479 .path(icon.path())
480 .size(size)
481 .flex_none()
482 .text_color(self.decoration_color.color(cx))
483 };
484
485 let decoration_knockout = |icon: IconName| {
486 svg()
487 .absolute()
488 .top(-rems_from_px(2.))
489 .left(-rems_from_px(3.))
490 .path(icon.path())
491 .size(size + rems_from_px(2.))
492 .flex_none()
493 .text_color(background)
494 };
495
496 div()
497 .relative()
498 .size(self.icon.size)
499 .child(self.icon)
500 .child(decoration_knockout(decoration_icon))
501 .child(decoration_svg(decoration_icon))
502 }
503}
504
505#[derive(IntoElement)]
506pub struct IconWithIndicator {
507 icon: Icon,
508 indicator: Option<Indicator>,
509 indicator_border_color: Option<Hsla>,
510}
511
512impl IconWithIndicator {
513 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
514 Self {
515 icon,
516 indicator,
517 indicator_border_color: None,
518 }
519 }
520
521 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
522 self.indicator = indicator;
523 self
524 }
525
526 pub fn indicator_color(mut self, color: Color) -> Self {
527 if let Some(indicator) = self.indicator.as_mut() {
528 indicator.color = color;
529 }
530 self
531 }
532
533 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
534 self.indicator_border_color = color;
535 self
536 }
537}
538
539impl RenderOnce for IconWithIndicator {
540 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
541 let indicator_border_color = self
542 .indicator_border_color
543 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
544
545 div()
546 .relative()
547 .child(self.icon)
548 .when_some(self.indicator, |this, indicator| {
549 this.child(
550 div()
551 .absolute()
552 .w_2()
553 .h_2()
554 .border_1()
555 .border_color(indicator_border_color)
556 .rounded_full()
557 .bottom_neg_0p5()
558 .right_neg_1()
559 .child(indicator),
560 )
561 })
562 }
563}