1use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
2use serde::{Deserialize, Serialize};
3use strum::{EnumIter, EnumString, IntoStaticStr};
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 /// Returns the individual components of the square that contains this [`IconSize`].
80 ///
81 /// The returned tuple contains:
82 /// 1. The length of one side of the square
83 /// 2. The padding of one side of the square
84 pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
85 let icon_size = self.rems() * cx.rem_size();
86 let padding = match self {
87 IconSize::Indicator => Spacing::None.px(cx),
88 IconSize::XSmall => Spacing::XSmall.px(cx),
89 IconSize::Small => Spacing::XSmall.px(cx),
90 IconSize::Medium => Spacing::XSmall.px(cx),
91 };
92
93 (icon_size, padding)
94 }
95
96 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
97 pub fn square(&self, cx: &mut WindowContext) -> Pixels {
98 let (icon_size, padding) = self.square_components(cx);
99
100 icon_size + padding * 2.
101 }
102}
103
104#[derive(
105 Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize,
106)]
107pub enum IconName {
108 Ai,
109 AiAnthropic,
110 AiOpenAi,
111 AiGoogle,
112 AiOllama,
113 AiZed,
114 ArrowCircle,
115 ArrowDown,
116 ArrowDownFromLine,
117 ArrowLeft,
118 ArrowRight,
119 ArrowUp,
120 ArrowUpFromLine,
121 ArrowUpRight,
122 AtSign,
123 AudioOff,
124 AudioOn,
125 Backspace,
126 Bell,
127 BellDot,
128 BellOff,
129 BellRing,
130 Bolt,
131 Book,
132 BookCopy,
133 BookPlus,
134 CaseSensitive,
135 Check,
136 ChevronDown,
137 /// This chevron indicates a popover menu.
138 ChevronDownSmall,
139 ChevronLeft,
140 ChevronRight,
141 ChevronUp,
142 ChevronUpDown,
143 Close,
144 Code,
145 Collab,
146 Command,
147 Context,
148 Control,
149 Copilot,
150 CopilotDisabled,
151 CopilotError,
152 CopilotInit,
153 Copy,
154 CountdownTimer,
155 Dash,
156 Delete,
157 Disconnected,
158 Download,
159 Ellipsis,
160 EllipsisVertical,
161 Envelope,
162 Escape,
163 ExclamationTriangle,
164 Exit,
165 ExpandVertical,
166 ExternalLink,
167 Eye,
168 File,
169 FileDoc,
170 FileGeneric,
171 FileGit,
172 FileLock,
173 FileRust,
174 FileToml,
175 FileTree,
176 FileText,
177 FileCode,
178 Filter,
179 Folder,
180 FolderOpen,
181 FolderX,
182 Font,
183 FontSize,
184 FontWeight,
185 Github,
186 GenericMinimize,
187 GenericMaximize,
188 GenericClose,
189 GenericRestore,
190 Hash,
191 HistoryRerun,
192 Indicator,
193 IndicatorX,
194 InlayHint,
195 Library,
196 LineHeight,
197 Link,
198 ListTree,
199 MagnifyingGlass,
200 MailOpen,
201 Maximize,
202 Menu,
203 MessageBubbles,
204 Mic,
205 MicMute,
206 Microscope,
207 Minimize,
208 Option,
209 PageDown,
210 PageUp,
211 Pencil,
212 Person,
213 Play,
214 Plus,
215 Public,
216 PullRequest,
217 Quote,
218 Regex,
219 ReplNeutral,
220 Replace,
221 ReplaceAll,
222 ReplaceNext,
223 ReplyArrowRight,
224 Rerun,
225 Return,
226 Reveal,
227 Route,
228 RotateCcw,
229 RotateCw,
230 Save,
231 Screen,
232 SearchSelection,
233 SearchCode,
234 SelectAll,
235 Server,
236 Settings,
237 Shift,
238 Slash,
239 SlashSquare,
240 Sliders,
241 SlidersAlt,
242 Snip,
243 Space,
244 Sparkle,
245 SparkleAlt,
246 SparkleFilled,
247 Spinner,
248 Split,
249 Star,
250 StarFilled,
251 Stop,
252 Strikethrough,
253 Supermaven,
254 SupermavenDisabled,
255 SupermavenError,
256 SupermavenInit,
257 Tab,
258 Terminal,
259 TextCursor,
260 TextSelect,
261 Trash,
262 TriangleRight,
263 Undo,
264 Update,
265 WholeWord,
266 XCircle,
267 ZedAssistant,
268 ZedAssistantFilled,
269 ZedXCopilot,
270 Visible,
271}
272
273impl IconName {
274 pub fn path(self) -> &'static str {
275 match self {
276 IconName::Ai => "icons/ai.svg",
277 IconName::AiAnthropic => "icons/ai_anthropic.svg",
278 IconName::AiOpenAi => "icons/ai_open_ai.svg",
279 IconName::AiGoogle => "icons/ai_google.svg",
280 IconName::AiOllama => "icons/ai_ollama.svg",
281 IconName::AiZed => "icons/ai_zed.svg",
282 IconName::ArrowCircle => "icons/arrow_circle.svg",
283 IconName::ArrowDown => "icons/arrow_down.svg",
284 IconName::ArrowDownFromLine => "icons/arrow_down_from_line.svg",
285 IconName::ArrowLeft => "icons/arrow_left.svg",
286 IconName::ArrowRight => "icons/arrow_right.svg",
287 IconName::ArrowUp => "icons/arrow_up.svg",
288 IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
289 IconName::ArrowUpRight => "icons/arrow_up_right.svg",
290 IconName::AtSign => "icons/at_sign.svg",
291 IconName::AudioOff => "icons/speaker_off.svg",
292 IconName::AudioOn => "icons/speaker_loud.svg",
293 IconName::Backspace => "icons/backspace.svg",
294 IconName::Bell => "icons/bell.svg",
295 IconName::BellDot => "icons/bell_dot.svg",
296 IconName::BellOff => "icons/bell_off.svg",
297 IconName::BellRing => "icons/bell_ring.svg",
298 IconName::Bolt => "icons/bolt.svg",
299 IconName::Book => "icons/book.svg",
300 IconName::BookCopy => "icons/book_copy.svg",
301 IconName::BookPlus => "icons/book_plus.svg",
302 IconName::CaseSensitive => "icons/case_insensitive.svg",
303 IconName::Check => "icons/check.svg",
304 IconName::ChevronDown => "icons/chevron_down.svg",
305 IconName::ChevronDownSmall => "icons/chevron_down_small.svg",
306 IconName::ChevronLeft => "icons/chevron_left.svg",
307 IconName::ChevronRight => "icons/chevron_right.svg",
308 IconName::ChevronUp => "icons/chevron_up.svg",
309 IconName::ChevronUpDown => "icons/chevron_up_down.svg",
310 IconName::Close => "icons/x.svg",
311 IconName::Code => "icons/code.svg",
312 IconName::Collab => "icons/user_group_16.svg",
313 IconName::Command => "icons/command.svg",
314 IconName::Context => "icons/context.svg",
315 IconName::Control => "icons/control.svg",
316 IconName::Copilot => "icons/copilot.svg",
317 IconName::CopilotDisabled => "icons/copilot_disabled.svg",
318 IconName::CopilotError => "icons/copilot_error.svg",
319 IconName::CopilotInit => "icons/copilot_init.svg",
320 IconName::Copy => "icons/copy.svg",
321 IconName::CountdownTimer => "icons/countdown_timer.svg",
322 IconName::Dash => "icons/dash.svg",
323 IconName::Delete => "icons/delete.svg",
324 IconName::Disconnected => "icons/disconnected.svg",
325 IconName::Download => "icons/download.svg",
326 IconName::Ellipsis => "icons/ellipsis.svg",
327 IconName::EllipsisVertical => "icons/ellipsis_vertical.svg",
328 IconName::Envelope => "icons/feedback.svg",
329 IconName::Escape => "icons/escape.svg",
330 IconName::ExclamationTriangle => "icons/warning.svg",
331 IconName::Exit => "icons/exit.svg",
332 IconName::ExpandVertical => "icons/expand_vertical.svg",
333 IconName::ExternalLink => "icons/external_link.svg",
334 IconName::Eye => "icons/eye.svg",
335 IconName::File => "icons/file.svg",
336 IconName::FileDoc => "icons/file_icons/book.svg",
337 IconName::FileGeneric => "icons/file_icons/file.svg",
338 IconName::FileGit => "icons/file_icons/git.svg",
339 IconName::FileLock => "icons/file_icons/lock.svg",
340 IconName::FileRust => "icons/file_icons/rust.svg",
341 IconName::FileToml => "icons/file_icons/toml.svg",
342 IconName::FileTree => "icons/project.svg",
343 IconName::FileCode => "icons/file_code.svg",
344 IconName::FileText => "icons/file_text.svg",
345 IconName::Filter => "icons/filter.svg",
346 IconName::Folder => "icons/file_icons/folder.svg",
347 IconName::FolderOpen => "icons/file_icons/folder_open.svg",
348 IconName::FolderX => "icons/stop_sharing.svg",
349 IconName::Font => "icons/font.svg",
350 IconName::FontSize => "icons/font_size.svg",
351 IconName::FontWeight => "icons/font_weight.svg",
352 IconName::Github => "icons/github.svg",
353 IconName::GenericMinimize => "icons/generic_minimize.svg",
354 IconName::GenericMaximize => "icons/generic_maximize.svg",
355 IconName::GenericClose => "icons/generic_close.svg",
356 IconName::GenericRestore => "icons/generic_restore.svg",
357 IconName::Hash => "icons/hash.svg",
358 IconName::HistoryRerun => "icons/history_rerun.svg",
359 IconName::Indicator => "icons/indicator.svg",
360 IconName::IndicatorX => "icons/indicator_x.svg",
361 IconName::InlayHint => "icons/inlay_hint.svg",
362 IconName::Library => "icons/library.svg",
363 IconName::LineHeight => "icons/line_height.svg",
364 IconName::Link => "icons/link.svg",
365 IconName::ListTree => "icons/list_tree.svg",
366 IconName::MagnifyingGlass => "icons/magnifying_glass.svg",
367 IconName::MailOpen => "icons/mail_open.svg",
368 IconName::Maximize => "icons/maximize.svg",
369 IconName::Menu => "icons/menu.svg",
370 IconName::MessageBubbles => "icons/conversations.svg",
371 IconName::Mic => "icons/mic.svg",
372 IconName::MicMute => "icons/mic_mute.svg",
373 IconName::Microscope => "icons/microscope.svg",
374 IconName::Minimize => "icons/minimize.svg",
375 IconName::Option => "icons/option.svg",
376 IconName::PageDown => "icons/page_down.svg",
377 IconName::PageUp => "icons/page_up.svg",
378 IconName::Pencil => "icons/pencil.svg",
379 IconName::Person => "icons/person.svg",
380 IconName::Play => "icons/play.svg",
381 IconName::Plus => "icons/plus.svg",
382 IconName::Public => "icons/public.svg",
383 IconName::PullRequest => "icons/pull_request.svg",
384 IconName::Quote => "icons/quote.svg",
385 IconName::Regex => "icons/regex.svg",
386 IconName::ReplNeutral => "icons/repl_neutral.svg",
387 IconName::Replace => "icons/replace.svg",
388 IconName::ReplaceAll => "icons/replace_all.svg",
389 IconName::ReplaceNext => "icons/replace_next.svg",
390 IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
391 IconName::Rerun => "icons/rerun.svg",
392 IconName::Return => "icons/return.svg",
393 IconName::Reveal => "icons/reveal.svg",
394 IconName::RotateCcw => "icons/rotate_ccw.svg",
395 IconName::RotateCw => "icons/rotate_cw.svg",
396 IconName::Route => "icons/route.svg",
397 IconName::Save => "icons/save.svg",
398 IconName::Screen => "icons/desktop.svg",
399 IconName::SearchSelection => "icons/search_selection.svg",
400 IconName::SearchCode => "icons/search_code.svg",
401 IconName::SelectAll => "icons/select_all.svg",
402 IconName::Server => "icons/server.svg",
403 IconName::Settings => "icons/file_icons/settings.svg",
404 IconName::Shift => "icons/shift.svg",
405 IconName::Slash => "icons/slash.svg",
406 IconName::SlashSquare => "icons/slash_square.svg",
407 IconName::Sliders => "icons/sliders.svg",
408 IconName::SlidersAlt => "icons/sliders-alt.svg",
409 IconName::Snip => "icons/snip.svg",
410 IconName::Space => "icons/space.svg",
411 IconName::Sparkle => "icons/sparkle.svg",
412 IconName::SparkleAlt => "icons/sparkle_alt.svg",
413 IconName::SparkleFilled => "icons/sparkle_filled.svg",
414 IconName::Spinner => "icons/spinner.svg",
415 IconName::Split => "icons/split.svg",
416 IconName::Star => "icons/star.svg",
417 IconName::StarFilled => "icons/star_filled.svg",
418 IconName::Stop => "icons/stop.svg",
419 IconName::Strikethrough => "icons/strikethrough.svg",
420 IconName::Supermaven => "icons/supermaven.svg",
421 IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",
422 IconName::SupermavenError => "icons/supermaven_error.svg",
423 IconName::SupermavenInit => "icons/supermaven_init.svg",
424 IconName::Tab => "icons/tab.svg",
425 IconName::Terminal => "icons/terminal.svg",
426 IconName::TextCursor => "icons/text-cursor.svg",
427 IconName::TextSelect => "icons/text_select.svg",
428 IconName::Trash => "icons/trash.svg",
429 IconName::TriangleRight => "icons/triangle_right.svg",
430 IconName::Update => "icons/update.svg",
431 IconName::Undo => "icons/undo.svg",
432 IconName::WholeWord => "icons/word_search.svg",
433 IconName::XCircle => "icons/error.svg",
434 IconName::ZedAssistant => "icons/zed_assistant.svg",
435 IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
436 IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
437 IconName::Visible => "icons/visible.svg",
438 }
439 }
440}
441
442#[derive(IntoElement)]
443pub struct Icon {
444 path: SharedString,
445 color: Color,
446 size: Rems,
447 transformation: Transformation,
448}
449
450impl Icon {
451 pub fn new(icon: IconName) -> Self {
452 Self {
453 path: icon.path().into(),
454 color: Color::default(),
455 size: IconSize::default().rems(),
456 transformation: Transformation::default(),
457 }
458 }
459
460 pub fn from_path(path: impl Into<SharedString>) -> Self {
461 Self {
462 path: path.into(),
463 color: Color::default(),
464 size: IconSize::default().rems(),
465 transformation: Transformation::default(),
466 }
467 }
468
469 pub fn color(mut self, color: Color) -> Self {
470 self.color = color;
471 self
472 }
473
474 pub fn size(mut self, size: IconSize) -> Self {
475 self.size = size.rems();
476 self
477 }
478
479 /// Sets a custom size for the icon, in [`Rems`].
480 ///
481 /// Not to be exposed outside of the `ui` crate.
482 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
483 self.size = size;
484 self
485 }
486
487 pub fn transform(mut self, transformation: Transformation) -> Self {
488 self.transformation = transformation;
489 self
490 }
491}
492
493impl RenderOnce for Icon {
494 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
495 svg()
496 .with_transformation(self.transformation)
497 .size(self.size)
498 .flex_none()
499 .path(self.path)
500 .text_color(self.color.color(cx))
501 }
502}
503
504#[derive(IntoElement)]
505pub struct DecoratedIcon {
506 icon: Icon,
507 decoration: IconDecoration,
508 decoration_color: Color,
509 parent_background: Option<Hsla>,
510}
511
512impl DecoratedIcon {
513 pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
514 Self {
515 icon,
516 decoration,
517 decoration_color: Color::Default,
518 parent_background: None,
519 }
520 }
521
522 pub fn decoration_color(mut self, color: Color) -> Self {
523 self.decoration_color = color;
524 self
525 }
526
527 pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
528 self.parent_background = background;
529 self
530 }
531}
532
533impl RenderOnce for DecoratedIcon {
534 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
535 let background = self
536 .parent_background
537 .unwrap_or(cx.theme().colors().background);
538
539 let size = self.icon.size;
540
541 let decoration_icon = match self.decoration {
542 IconDecoration::Strikethrough => IconName::Strikethrough,
543 IconDecoration::IndicatorDot => IconName::Indicator,
544 IconDecoration::X => IconName::IndicatorX,
545 };
546
547 let decoration_svg = |icon: IconName| {
548 svg()
549 .absolute()
550 .top_0()
551 .left_0()
552 .path(icon.path())
553 .size(size)
554 .flex_none()
555 .text_color(self.decoration_color.color(cx))
556 };
557
558 let decoration_knockout = |icon: IconName| {
559 svg()
560 .absolute()
561 .top(-rems_from_px(2.))
562 .left(-rems_from_px(3.))
563 .path(icon.path())
564 .size(size + rems_from_px(2.))
565 .flex_none()
566 .text_color(background)
567 };
568
569 div()
570 .relative()
571 .size(self.icon.size)
572 .child(self.icon)
573 .child(decoration_knockout(decoration_icon))
574 .child(decoration_svg(decoration_icon))
575 }
576}
577
578#[derive(IntoElement)]
579pub struct IconWithIndicator {
580 icon: Icon,
581 indicator: Option<Indicator>,
582 indicator_border_color: Option<Hsla>,
583}
584
585impl IconWithIndicator {
586 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
587 Self {
588 icon,
589 indicator,
590 indicator_border_color: None,
591 }
592 }
593
594 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
595 self.indicator = indicator;
596 self
597 }
598
599 pub fn indicator_color(mut self, color: Color) -> Self {
600 if let Some(indicator) = self.indicator.as_mut() {
601 indicator.color = color;
602 }
603 self
604 }
605
606 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
607 self.indicator_border_color = color;
608 self
609 }
610}
611
612impl RenderOnce for IconWithIndicator {
613 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
614 let indicator_border_color = self
615 .indicator_border_color
616 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
617
618 div()
619 .relative()
620 .child(self.icon)
621 .when_some(self.indicator, |this, indicator| {
622 this.child(
623 div()
624 .absolute()
625 .w_2()
626 .h_2()
627 .border_1()
628 .border_color(indicator_border_color)
629 .rounded_full()
630 .bottom_neg_0p5()
631 .right_neg_1()
632 .child(indicator),
633 )
634 })
635 }
636}