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