1mod decorated_icon;
2mod icon_decoration;
3
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7pub use decorated_icon::*;
8use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
9pub use icon_decoration::*;
10use serde::{Deserialize, Serialize};
11use strum::{EnumIter, EnumString, IntoStaticStr};
12use ui_macros::DerivePathStr;
13
14use crate::{prelude::*, Indicator};
15
16#[derive(IntoElement)]
17pub enum AnyIcon {
18 Icon(Icon),
19 AnimatedIcon(AnimationElement<Icon>),
20}
21
22impl AnyIcon {
23 /// Returns a new [`AnyIcon`] after applying the given mapping function
24 /// to the contained [`Icon`].
25 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
26 match self {
27 Self::Icon(icon) => Self::Icon(f(icon)),
28 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
29 }
30 }
31}
32
33impl From<Icon> for AnyIcon {
34 fn from(value: Icon) -> Self {
35 Self::Icon(value)
36 }
37}
38
39impl From<AnimationElement<Icon>> for AnyIcon {
40 fn from(value: AnimationElement<Icon>) -> Self {
41 Self::AnimatedIcon(value)
42 }
43}
44
45impl RenderOnce for AnyIcon {
46 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
47 match self {
48 Self::Icon(icon) => icon.into_any_element(),
49 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
50 }
51 }
52}
53
54#[derive(Default, PartialEq, Copy, Clone)]
55pub enum IconSize {
56 /// 10px
57 Indicator,
58 /// 12px
59 XSmall,
60 /// 14px
61 Small,
62 #[default]
63 /// 16px
64 Medium,
65 /// 48px
66 XLarge,
67 Custom(Rems),
68}
69
70impl IconSize {
71 pub fn rems(self) -> Rems {
72 match self {
73 IconSize::Indicator => rems_from_px(10.),
74 IconSize::XSmall => rems_from_px(12.),
75 IconSize::Small => rems_from_px(14.),
76 IconSize::Medium => rems_from_px(16.),
77 IconSize::XLarge => rems_from_px(48.),
78 IconSize::Custom(size) => size,
79 }
80 }
81
82 /// Returns the individual components of the square that contains this [`IconSize`].
83 ///
84 /// The returned tuple contains:
85 /// 1. The length of one side of the square
86 /// 2. The padding of one side of the square
87 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
88 let icon_size = self.rems() * window.rem_size();
89 let padding = match self {
90 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
91 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
92 IconSize::Small => DynamicSpacing::Base02.px(cx),
93 IconSize::Medium => DynamicSpacing::Base02.px(cx),
94 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
95 // TODO: Wire into dynamic spacing
96 IconSize::Custom(size) => size.to_pixels(window.rem_size()),
97 };
98
99 (icon_size, padding)
100 }
101
102 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
103 pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
104 let (icon_size, padding) = self.square_components(window, cx);
105
106 icon_size + padding * 2.
107 }
108}
109
110#[derive(
111 Debug,
112 PartialEq,
113 Eq,
114 Copy,
115 Clone,
116 EnumIter,
117 EnumString,
118 IntoStaticStr,
119 Serialize,
120 Deserialize,
121 DerivePathStr,
122)]
123#[strum(serialize_all = "snake_case")]
124#[path_str(prefix = "icons", suffix = ".svg")]
125pub enum IconName {
126 Ai,
127 AiAnthropic,
128 AiBedrock,
129 AiAnthropicHosted,
130 AiDeepSeek,
131 AiGoogle,
132 AiLmStudio,
133 AiMistral,
134 AiOllama,
135 AiOpenAi,
136 AiZed,
137 ArrowCircle,
138 ArrowDown,
139 ArrowDownFromLine,
140 ArrowLeft,
141 ArrowRight,
142 ArrowUp,
143 ArrowUpFromLine,
144 ArrowUpRight,
145 AtSign,
146 AudioOff,
147 AudioOn,
148 Backspace,
149 Bell,
150 BellDot,
151 BellOff,
152 BellRing,
153 Blocks,
154 Bolt,
155 Book,
156 BookCopy,
157 BookPlus,
158 CaseSensitive,
159 Check,
160 ChevronDown,
161 /// This chevron indicates a popover menu.
162 ChevronDownSmall,
163 ChevronLeft,
164 ChevronRight,
165 ChevronUp,
166 ChevronUpDown,
167 Circle,
168 Close,
169 Code,
170 Command,
171 Context,
172 Control,
173 Copilot,
174 CopilotDisabled,
175 CopilotError,
176 CopilotInit,
177 Copy,
178 CountdownTimer,
179 CursorIBeam,
180 Dash,
181 DatabaseZap,
182 Delete,
183 Diff,
184 Disconnected,
185 Download,
186 Ellipsis,
187 EllipsisVertical,
188 Envelope,
189 Eraser,
190 Escape,
191 ExpandVertical,
192 Exit,
193 ExternalLink,
194 Eye,
195 File,
196 FileCode,
197 FileDoc,
198 FileDiff,
199 FileGeneric,
200 FileGit,
201 FileLock,
202 FileRust,
203 FileSearch,
204 FileText,
205 FileToml,
206 FileTree,
207 Filter,
208 Folder,
209 FolderOpen,
210 FolderX,
211 Font,
212 FontSize,
213 FontWeight,
214 GenericClose,
215 GenericMaximize,
216 GenericMinimize,
217 GenericRestore,
218 Github,
219 Globe,
220 GitBranch,
221 GitBranchSmall,
222 Hash,
223 HistoryRerun,
224 Indicator,
225 Info,
226 InlayHint,
227 Keyboard,
228 Library,
229 LineHeight,
230 Link,
231 ListTree,
232 ListX,
233 LockOutlined,
234 MagnifyingGlass,
235 MailOpen,
236 Maximize,
237 Menu,
238 MessageBubbles,
239 MessageCircle,
240 Cloud,
241 Mic,
242 MicMute,
243 Microscope,
244 Minimize,
245 Option,
246 PageDown,
247 PageUp,
248 PanelLeft,
249 PanelRight,
250 Pencil,
251 Person,
252 PersonCircle,
253 PhoneIncoming,
254 Pin,
255 Play,
256 Plus,
257 PocketKnife,
258 Public,
259 PullRequest,
260 Quote,
261 RefreshTitle,
262 Regex,
263 ReplNeutral,
264 Replace,
265 ReplaceAll,
266 ReplaceNext,
267 ReplyArrowRight,
268 Rerun,
269 Return,
270 Reveal,
271 RotateCcw,
272 RotateCw,
273 Route,
274 Save,
275 Screen,
276 SearchCode,
277 SearchSelection,
278 SelectAll,
279 Server,
280 Settings,
281 SettingsAlt,
282 Shift,
283 Slash,
284 SlashSquare,
285 Sliders,
286 SlidersVertical,
287 Snip,
288 Space,
289 Sparkle,
290 SparkleAlt,
291 SparkleFilled,
292 Spinner,
293 Split,
294 SquareDot,
295 SquareMinus,
296 SquarePlus,
297 Star,
298 StarFilled,
299 Stop,
300 Strikethrough,
301 Supermaven,
302 SupermavenDisabled,
303 SupermavenError,
304 SupermavenInit,
305 SwatchBook,
306 Tab,
307 Terminal,
308 TextSnippet,
309 ThumbsUp,
310 ThumbsDown,
311 Trash,
312 TrashAlt,
313 Triangle,
314 TriangleRight,
315 Undo,
316 Unpin,
317 Update,
318 UserGroup,
319 Visible,
320 Wand,
321 Warning,
322 WholeWord,
323 X,
324 XCircle,
325 ZedAssistant,
326 ZedAssistant2,
327 ZedAssistantFilled,
328 ZedPredict,
329 ZedPredictUp,
330 ZedPredictDown,
331 ZedPredictDisabled,
332 ZedPredictError,
333 ZedXCopilot,
334}
335
336impl From<IconName> for Icon {
337 fn from(icon: IconName) -> Self {
338 Icon::new(icon)
339 }
340}
341
342/// The source of an icon.
343enum IconSource {
344 /// An SVG embedded in the Zed binary.
345 Svg(SharedString),
346 /// An image file located at the specified path.
347 ///
348 /// Currently our SVG renderer is missing support for the following features:
349 /// 1. Loading SVGs from external files.
350 /// 2. Rendering polychrome SVGs.
351 ///
352 /// In order to support icon themes, we render the icons as images instead.
353 Image(Arc<Path>),
354}
355
356impl IconSource {
357 fn from_path(path: impl Into<SharedString>) -> Self {
358 let path = path.into();
359 if path.starts_with("icons/file_icons") {
360 Self::Svg(path)
361 } else {
362 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
363 }
364 }
365}
366
367#[derive(IntoElement, IntoComponent)]
368pub struct Icon {
369 source: IconSource,
370 color: Color,
371 size: Rems,
372 transformation: Transformation,
373}
374
375impl Icon {
376 pub fn new(icon: IconName) -> Self {
377 Self {
378 source: IconSource::Svg(icon.path().into()),
379 color: Color::default(),
380 size: IconSize::default().rems(),
381 transformation: Transformation::default(),
382 }
383 }
384
385 pub fn from_path(path: impl Into<SharedString>) -> Self {
386 Self {
387 source: IconSource::from_path(path),
388 color: Color::default(),
389 size: IconSize::default().rems(),
390 transformation: Transformation::default(),
391 }
392 }
393
394 pub fn color(mut self, color: Color) -> Self {
395 self.color = color;
396 self
397 }
398
399 pub fn size(mut self, size: IconSize) -> Self {
400 self.size = size.rems();
401 self
402 }
403
404 /// Sets a custom size for the icon, in [`Rems`].
405 ///
406 /// Not to be exposed outside of the `ui` crate.
407 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
408 self.size = size;
409 self
410 }
411
412 pub fn transform(mut self, transformation: Transformation) -> Self {
413 self.transformation = transformation;
414 self
415 }
416}
417
418impl RenderOnce for Icon {
419 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
420 match self.source {
421 IconSource::Svg(path) => svg()
422 .with_transformation(self.transformation)
423 .size(self.size)
424 .flex_none()
425 .path(path)
426 .text_color(self.color.color(cx))
427 .into_any_element(),
428 IconSource::Image(path) => img(path)
429 .size(self.size)
430 .flex_none()
431 .text_color(self.color.color(cx))
432 .into_any_element(),
433 }
434 }
435}
436
437#[derive(IntoElement)]
438pub struct IconWithIndicator {
439 icon: Icon,
440 indicator: Option<Indicator>,
441 indicator_border_color: Option<Hsla>,
442}
443
444impl IconWithIndicator {
445 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
446 Self {
447 icon,
448 indicator,
449 indicator_border_color: None,
450 }
451 }
452
453 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
454 self.indicator = indicator;
455 self
456 }
457
458 pub fn indicator_color(mut self, color: Color) -> Self {
459 if let Some(indicator) = self.indicator.as_mut() {
460 indicator.color = color;
461 }
462 self
463 }
464
465 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
466 self.indicator_border_color = color;
467 self
468 }
469}
470
471impl RenderOnce for IconWithIndicator {
472 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
473 let indicator_border_color = self
474 .indicator_border_color
475 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
476
477 div()
478 .relative()
479 .child(self.icon)
480 .when_some(self.indicator, |this, indicator| {
481 this.child(
482 div()
483 .absolute()
484 .size_2p5()
485 .border_2()
486 .border_color(indicator_border_color)
487 .rounded_full()
488 .bottom_neg_0p5()
489 .right_neg_0p5()
490 .child(indicator),
491 )
492 })
493 }
494}
495
496// View this component preview using `workspace: open component-preview`
497impl ComponentPreview for Icon {
498 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
499 v_flex()
500 .gap_6()
501 .children(vec![
502 example_group_with_title(
503 "Sizes",
504 vec![
505 single_example("Default", Icon::new(IconName::Star).into_any_element()),
506 single_example(
507 "Small",
508 Icon::new(IconName::Star)
509 .size(IconSize::Small)
510 .into_any_element(),
511 ),
512 single_example(
513 "Large",
514 Icon::new(IconName::Star)
515 .size(IconSize::XLarge)
516 .into_any_element(),
517 ),
518 ],
519 ),
520 example_group_with_title(
521 "Colors",
522 vec![
523 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
524 single_example(
525 "Custom Color",
526 Icon::new(IconName::Bell)
527 .color(Color::Error)
528 .into_any_element(),
529 ),
530 ],
531 ),
532 ])
533 .into_any_element()
534 }
535}