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