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