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