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