1#![allow(missing_docs)]
2
3mod decorated_icon;
4mod icon_decoration;
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9pub use decorated_icon::*;
10use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
11pub use icon_decoration::*;
12use serde::{Deserialize, Serialize};
13use strum::{EnumIter, EnumString, IntoStaticStr};
14use ui_macros::DerivePathStr;
15
16use crate::{prelude::*, Indicator};
17
18#[derive(IntoElement)]
19pub enum AnyIcon {
20 Icon(Icon),
21 AnimatedIcon(AnimationElement<Icon>),
22}
23
24impl AnyIcon {
25 /// Returns a new [`AnyIcon`] after applying the given mapping function
26 /// to the contained [`Icon`].
27 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
28 match self {
29 Self::Icon(icon) => Self::Icon(f(icon)),
30 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
31 }
32 }
33}
34
35impl From<Icon> for AnyIcon {
36 fn from(value: Icon) -> Self {
37 Self::Icon(value)
38 }
39}
40
41impl From<AnimationElement<Icon>> for AnyIcon {
42 fn from(value: AnimationElement<Icon>) -> Self {
43 Self::AnimatedIcon(value)
44 }
45}
46
47impl RenderOnce for AnyIcon {
48 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
49 match self {
50 Self::Icon(icon) => icon.into_any_element(),
51 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
52 }
53 }
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 /// 48px
68 XLarge,
69 Custom(Rems),
70}
71
72impl IconSize {
73 pub fn rems(self) -> Rems {
74 match self {
75 IconSize::Indicator => rems_from_px(10.),
76 IconSize::XSmall => rems_from_px(12.),
77 IconSize::Small => rems_from_px(14.),
78 IconSize::Medium => rems_from_px(16.),
79 IconSize::XLarge => rems_from_px(48.),
80 IconSize::Custom(size) => size,
81 }
82 }
83
84 /// Returns the individual components of the square that contains this [`IconSize`].
85 ///
86 /// The returned tuple contains:
87 /// 1. The length of one side of the square
88 /// 2. The padding of one side of the square
89 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
90 let icon_size = self.rems() * window.rem_size();
91 let padding = match self {
92 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
93 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
94 IconSize::Small => DynamicSpacing::Base02.px(cx),
95 IconSize::Medium => DynamicSpacing::Base02.px(cx),
96 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
97 // TODO: Wire into dynamic spacing
98 IconSize::Custom(size) => size.to_pixels(window.rem_size()),
99 };
100
101 (icon_size, padding)
102 }
103
104 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
105 pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
106 let (icon_size, padding) = self.square_components(window, cx);
107
108 icon_size + padding * 2.
109 }
110}
111
112#[derive(
113 Debug,
114 PartialEq,
115 Eq,
116 Copy,
117 Clone,
118 EnumIter,
119 EnumString,
120 IntoStaticStr,
121 Serialize,
122 Deserialize,
123 DerivePathStr,
124)]
125#[strum(serialize_all = "snake_case")]
126#[path_str(prefix = "icons", suffix = ".svg")]
127pub enum IconName {
128 Ai,
129 AiAnthropic,
130 AiAnthropicHosted,
131 AiDeepSeek,
132 AiGoogle,
133 AiLmStudio,
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 Hash,
222 HistoryRerun,
223 Indicator,
224 IndicatorX,
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 Mic,
241 MicMute,
242 Microscope,
243 Minimize,
244 Option,
245 PageDown,
246 PageUp,
247 PanelLeft,
248 PanelRight,
249 Pencil,
250 Person,
251 PersonCircle,
252 PhoneIncoming,
253 Pin,
254 Play,
255 Plus,
256 PocketKnife,
257 Public,
258 PullRequest,
259 Quote,
260 RefreshTitle,
261 Regex,
262 ReplNeutral,
263 Replace,
264 ReplaceAll,
265 ReplaceNext,
266 ReplyArrowRight,
267 Rerun,
268 Return,
269 Reveal,
270 RotateCcw,
271 RotateCw,
272 Route,
273 Save,
274 Screen,
275 SearchCode,
276 SearchSelection,
277 SelectAll,
278 Server,
279 Settings,
280 SettingsAlt,
281 Shift,
282 Slash,
283 SlashSquare,
284 Sliders,
285 SlidersVertical,
286 Snip,
287 Space,
288 Sparkle,
289 SparkleAlt,
290 SparkleFilled,
291 Spinner,
292 Split,
293 SquareDot,
294 SquareMinus,
295 SquarePlus,
296 Star,
297 StarFilled,
298 Stop,
299 Strikethrough,
300 Supermaven,
301 SupermavenDisabled,
302 SupermavenError,
303 SupermavenInit,
304 SwatchBook,
305 Tab,
306 Terminal,
307 TextSnippet,
308 ThumbsUp,
309 ThumbsDown,
310 Trash,
311 TrashAlt,
312 Triangle,
313 TriangleRight,
314 Undo,
315 Unpin,
316 Update,
317 UserGroup,
318 Visible,
319 Wand,
320 Warning,
321 WholeWord,
322 X,
323 XCircle,
324 ZedAssistant,
325 ZedAssistant2,
326 ZedAssistantFilled,
327 ZedPredict,
328 ZedPredictDisabled,
329 ZedXCopilot,
330}
331
332impl From<IconName> for Icon {
333 fn from(icon: IconName) -> Self {
334 Icon::new(icon)
335 }
336}
337
338/// The source of an icon.
339enum IconSource {
340 /// An SVG embedded in the Zed binary.
341 Svg(SharedString),
342 /// An image file located at the specified path.
343 ///
344 /// Currently our SVG renderer is missing support for the following features:
345 /// 1. Loading SVGs from external files.
346 /// 2. Rendering polychrome SVGs.
347 ///
348 /// In order to support icon themes, we render the icons as images instead.
349 Image(Arc<Path>),
350}
351
352impl IconSource {
353 fn from_path(path: impl Into<SharedString>) -> Self {
354 let path = path.into();
355 if path.starts_with("icons/file_icons") {
356 Self::Svg(path)
357 } else {
358 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
359 }
360 }
361}
362
363#[derive(IntoElement, IntoComponent)]
364pub struct Icon {
365 source: IconSource,
366 color: Color,
367 size: Rems,
368 transformation: Transformation,
369}
370
371impl Icon {
372 pub fn new(icon: IconName) -> Self {
373 Self {
374 source: IconSource::Svg(icon.path().into()),
375 color: Color::default(),
376 size: IconSize::default().rems(),
377 transformation: Transformation::default(),
378 }
379 }
380
381 pub fn from_path(path: impl Into<SharedString>) -> Self {
382 Self {
383 source: IconSource::from_path(path),
384 color: Color::default(),
385 size: IconSize::default().rems(),
386 transformation: Transformation::default(),
387 }
388 }
389
390 pub fn color(mut self, color: Color) -> Self {
391 self.color = color;
392 self
393 }
394
395 pub fn size(mut self, size: IconSize) -> Self {
396 self.size = size.rems();
397 self
398 }
399
400 /// Sets a custom size for the icon, in [`Rems`].
401 ///
402 /// Not to be exposed outside of the `ui` crate.
403 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
404 self.size = size;
405 self
406 }
407
408 pub fn transform(mut self, transformation: Transformation) -> Self {
409 self.transformation = transformation;
410 self
411 }
412}
413
414impl RenderOnce for Icon {
415 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
416 match self.source {
417 IconSource::Svg(path) => svg()
418 .with_transformation(self.transformation)
419 .size(self.size)
420 .flex_none()
421 .path(path)
422 .text_color(self.color.color(cx))
423 .into_any_element(),
424 IconSource::Image(path) => img(path)
425 .size(self.size)
426 .flex_none()
427 .text_color(self.color.color(cx))
428 .into_any_element(),
429 }
430 }
431}
432
433#[derive(IntoElement)]
434pub struct IconWithIndicator {
435 icon: Icon,
436 indicator: Option<Indicator>,
437 indicator_border_color: Option<Hsla>,
438}
439
440impl IconWithIndicator {
441 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
442 Self {
443 icon,
444 indicator,
445 indicator_border_color: None,
446 }
447 }
448
449 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
450 self.indicator = indicator;
451 self
452 }
453
454 pub fn indicator_color(mut self, color: Color) -> Self {
455 if let Some(indicator) = self.indicator.as_mut() {
456 indicator.color = color;
457 }
458 self
459 }
460
461 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
462 self.indicator_border_color = color;
463 self
464 }
465}
466
467impl RenderOnce for IconWithIndicator {
468 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
469 let indicator_border_color = self
470 .indicator_border_color
471 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
472
473 div()
474 .relative()
475 .child(self.icon)
476 .when_some(self.indicator, |this, indicator| {
477 this.child(
478 div()
479 .absolute()
480 .size_2p5()
481 .border_2()
482 .border_color(indicator_border_color)
483 .rounded_full()
484 .bottom_neg_0p5()
485 .right_neg_0p5()
486 .child(indicator),
487 )
488 })
489 }
490}
491
492impl ComponentPreview for Icon {
493 fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
494 v_flex()
495 .gap_6()
496 .children(vec![
497 example_group_with_title(
498 "Sizes",
499 vec![
500 single_example("Default", Icon::new(IconName::Star).into_any_element()),
501 single_example(
502 "Small",
503 Icon::new(IconName::Star)
504 .size(IconSize::Small)
505 .into_any_element(),
506 ),
507 single_example(
508 "Large",
509 Icon::new(IconName::Star)
510 .size(IconSize::XLarge)
511 .into_any_element(),
512 ),
513 ],
514 ),
515 example_group_with_title(
516 "Colors",
517 vec![
518 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
519 single_example(
520 "Custom Color",
521 Icon::new(IconName::Bell)
522 .color(Color::Error)
523 .into_any_element(),
524 ),
525 ],
526 ),
527 ])
528 .into_any_element()
529 }
530}