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