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, Hsla, IntoElement, Rems, Transformation};
11pub use icon_decoration::*;
12use serde::{Deserialize, Serialize};
13use strum::{EnumIter, EnumString, IntoStaticStr};
14use ui_macros::DerivePathStr;
15
16use crate::{
17 prelude::*,
18 traits::component_preview::{ComponentExample, ComponentPreview},
19 Indicator,
20};
21
22#[derive(IntoElement)]
23pub enum AnyIcon {
24 Icon(Icon),
25 AnimatedIcon(AnimationElement<Icon>),
26}
27
28impl AnyIcon {
29 /// Returns a new [`AnyIcon`] after applying the given mapping function
30 /// to the contained [`Icon`].
31 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
32 match self {
33 Self::Icon(icon) => Self::Icon(f(icon)),
34 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
35 }
36 }
37}
38
39impl From<Icon> for AnyIcon {
40 fn from(value: Icon) -> Self {
41 Self::Icon(value)
42 }
43}
44
45impl From<AnimationElement<Icon>> for AnyIcon {
46 fn from(value: AnimationElement<Icon>) -> Self {
47 Self::AnimatedIcon(value)
48 }
49}
50
51impl RenderOnce for AnyIcon {
52 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
53 match self {
54 Self::Icon(icon) => icon.into_any_element(),
55 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
56 }
57 }
58}
59
60#[derive(Default, PartialEq, Copy, Clone)]
61pub enum IconSize {
62 /// 10px
63 Indicator,
64 /// 12px
65 XSmall,
66 /// 14px
67 Small,
68 #[default]
69 /// 16px
70 Medium,
71 /// 48px
72 XLarge,
73 Custom(Pixels),
74}
75
76impl IconSize {
77 pub fn rems(self) -> Rems {
78 match self {
79 IconSize::Indicator => rems_from_px(10.),
80 IconSize::XSmall => rems_from_px(12.),
81 IconSize::Small => rems_from_px(14.),
82 IconSize::Medium => rems_from_px(16.),
83 IconSize::XLarge => rems_from_px(48.),
84 IconSize::Custom(size) => rems_from_px(size.into()),
85 }
86 }
87
88 /// Returns the individual components of the square that contains this [`IconSize`].
89 ///
90 /// The returned tuple contains:
91 /// 1. The length of one side of the square
92 /// 2. The padding of one side of the square
93 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
94 let icon_size = self.rems() * window.rem_size();
95 let padding = match self {
96 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
97 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
98 IconSize::Small => DynamicSpacing::Base02.px(cx),
99 IconSize::Medium => DynamicSpacing::Base02.px(cx),
100 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
101 // TODO: Wire into dynamic spacing
102 IconSize::Custom(size) => px(size.into()),
103 };
104
105 (icon_size, padding)
106 }
107
108 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
109 pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels {
110 let (icon_size, padding) = self.square_components(window, cx);
111
112 icon_size + padding * 2.
113 }
114}
115
116#[derive(
117 Debug,
118 PartialEq,
119 Eq,
120 Copy,
121 Clone,
122 EnumIter,
123 EnumString,
124 IntoStaticStr,
125 Serialize,
126 Deserialize,
127 DerivePathStr,
128)]
129#[strum(serialize_all = "snake_case")]
130#[path_str(prefix = "icons", suffix = ".svg")]
131pub enum IconName {
132 Ai,
133 AiAnthropic,
134 AiAnthropicHosted,
135 AiDeepSeek,
136 AiGoogle,
137 AiLmStudio,
138 AiOllama,
139 AiOpenAi,
140 AiZed,
141 ArrowCircle,
142 ArrowDown,
143 ArrowDownFromLine,
144 ArrowLeft,
145 ArrowRight,
146 ArrowUp,
147 ArrowUpFromLine,
148 ArrowUpRight,
149 AtSign,
150 AudioOff,
151 AudioOn,
152 Backspace,
153 Bell,
154 BellDot,
155 BellOff,
156 BellRing,
157 Blocks,
158 Bolt,
159 Book,
160 BookCopy,
161 BookPlus,
162 CaseSensitive,
163 Check,
164 ChevronDown,
165 /// This chevron indicates a popover menu.
166 ChevronDownSmall,
167 ChevronLeft,
168 ChevronRight,
169 ChevronUp,
170 ChevronUpDown,
171 Circle,
172 Close,
173 Code,
174 Command,
175 Context,
176 Control,
177 Copilot,
178 CopilotDisabled,
179 CopilotError,
180 CopilotInit,
181 Copy,
182 CountdownTimer,
183 CursorIBeam,
184 Dash,
185 DatabaseZap,
186 Delete,
187 Diff,
188 Disconnected,
189 Download,
190 Ellipsis,
191 EllipsisVertical,
192 Envelope,
193 Eraser,
194 Escape,
195 ExpandVertical,
196 Exit,
197 ExternalLink,
198 Eye,
199 File,
200 FileCode,
201 FileDoc,
202 FileDiff,
203 FileGeneric,
204 FileGit,
205 FileLock,
206 FileRust,
207 FileSearch,
208 FileText,
209 FileToml,
210 FileTree,
211 Filter,
212 Folder,
213 FolderOpen,
214 FolderX,
215 Font,
216 FontSize,
217 FontWeight,
218 GenericClose,
219 GenericMaximize,
220 GenericMinimize,
221 GenericRestore,
222 Github,
223 Globe,
224 GitBranch,
225 Hash,
226 HistoryRerun,
227 Indicator,
228 IndicatorX,
229 Info,
230 InlayHint,
231 Keyboard,
232 Library,
233 LineHeight,
234 Link,
235 ListTree,
236 ListX,
237 LockOutlined,
238 MagnifyingGlass,
239 MailOpen,
240 Maximize,
241 Menu,
242 MessageBubbles,
243 MessageCircle,
244 Mic,
245 MicMute,
246 Microscope,
247 Minimize,
248 Option,
249 PageDown,
250 PageUp,
251 PanelLeft,
252 PanelRight,
253 Pencil,
254 Person,
255 PersonCircle,
256 PhoneIncoming,
257 Pin,
258 Play,
259 Plus,
260 PocketKnife,
261 Public,
262 PullRequest,
263 Quote,
264 RefreshTitle,
265 Regex,
266 ReplNeutral,
267 Replace,
268 ReplaceAll,
269 ReplaceNext,
270 ReplyArrowRight,
271 Rerun,
272 Return,
273 Reveal,
274 RotateCcw,
275 RotateCw,
276 Route,
277 Save,
278 Screen,
279 SearchCode,
280 SearchSelection,
281 SelectAll,
282 Server,
283 Settings,
284 SettingsAlt,
285 Shift,
286 Slash,
287 SlashSquare,
288 Sliders,
289 SlidersVertical,
290 Snip,
291 Space,
292 Sparkle,
293 SparkleAlt,
294 SparkleFilled,
295 Spinner,
296 Split,
297 SquareDot,
298 SquareMinus,
299 SquarePlus,
300 Star,
301 StarFilled,
302 Stop,
303 Strikethrough,
304 Supermaven,
305 SupermavenDisabled,
306 SupermavenError,
307 SupermavenInit,
308 SwatchBook,
309 Tab,
310 Terminal,
311 TextSnippet,
312 ThumbsUp,
313 ThumbsDown,
314 Trash,
315 TrashAlt,
316 Triangle,
317 TriangleRight,
318 Undo,
319 Unpin,
320 Update,
321 UserGroup,
322 Visible,
323 Wand,
324 Warning,
325 WholeWord,
326 X,
327 XCircle,
328 ZedAssistant,
329 ZedAssistant2,
330 ZedAssistantFilled,
331 ZedPredict,
332 ZedPredictDisabled,
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)]
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
496impl ComponentPreview for Icon {
497 fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
498 let arrow_icons = vec![
499 IconName::ArrowDown,
500 IconName::ArrowLeft,
501 IconName::ArrowRight,
502 IconName::ArrowUp,
503 IconName::ArrowCircle,
504 ];
505
506 vec![example_group_with_title(
507 "Arrow Icons",
508 arrow_icons
509 .into_iter()
510 .map(|icon| {
511 let name = format!("{:?}", icon).to_string();
512 ComponentExample::new(name, Icon::new(icon))
513 })
514 .collect(),
515 )]
516 }
517}