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