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}
74
75impl IconSize {
76 pub fn rems(self) -> Rems {
77 match self {
78 IconSize::Indicator => rems_from_px(10.),
79 IconSize::XSmall => rems_from_px(12.),
80 IconSize::Small => rems_from_px(14.),
81 IconSize::Medium => rems_from_px(16.),
82 IconSize::XLarge => rems_from_px(48.),
83 }
84 }
85
86 /// Returns the individual components of the square that contains this [`IconSize`].
87 ///
88 /// The returned tuple contains:
89 /// 1. The length of one side of the square
90 /// 2. The padding of one side of the square
91 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
92 let icon_size = self.rems() * window.rem_size();
93 let padding = match self {
94 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
95 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
96 IconSize::Small => DynamicSpacing::Base02.px(cx),
97 IconSize::Medium => DynamicSpacing::Base02.px(cx),
98 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
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 Close,
168 Code,
169 Command,
170 Context,
171 Control,
172 Copilot,
173 CopilotDisabled,
174 CopilotError,
175 CopilotInit,
176 Copy,
177 CountdownTimer,
178 CursorIBeam,
179 Dash,
180 DatabaseZap,
181 Delete,
182 Diff,
183 Disconnected,
184 Download,
185 Ellipsis,
186 EllipsisVertical,
187 Envelope,
188 Eraser,
189 Escape,
190 ExpandVertical,
191 Exit,
192 ExternalLink,
193 Eye,
194 File,
195 FileCode,
196 FileDoc,
197 FileDiff,
198 FileGeneric,
199 FileGit,
200 FileLock,
201 FileRust,
202 FileSearch,
203 FileText,
204 FileToml,
205 FileTree,
206 Filter,
207 Folder,
208 FolderOpen,
209 FolderX,
210 Font,
211 FontSize,
212 FontWeight,
213 GenericClose,
214 GenericMaximize,
215 GenericMinimize,
216 GenericRestore,
217 Github,
218 Globe,
219 GitBranch,
220 Hash,
221 HistoryRerun,
222 Indicator,
223 IndicatorX,
224 Info,
225 InlayHint,
226 Keyboard,
227 Library,
228 LineHeight,
229 Link,
230 ListTree,
231 ListX,
232 MagnifyingGlass,
233 MailOpen,
234 Maximize,
235 Menu,
236 MessageBubbles,
237 MessageCircle,
238 Mic,
239 MicMute,
240 Microscope,
241 Minimize,
242 Option,
243 PageDown,
244 PageUp,
245 PanelLeft,
246 PanelRight,
247 Pencil,
248 Person,
249 PersonCircle,
250 PhoneIncoming,
251 Pin,
252 Play,
253 Plus,
254 PocketKnife,
255 Public,
256 PullRequest,
257 Quote,
258 RefreshTitle,
259 Regex,
260 ReplNeutral,
261 Replace,
262 ReplaceAll,
263 ReplaceNext,
264 ReplyArrowRight,
265 Rerun,
266 Return,
267 Reveal,
268 RotateCcw,
269 RotateCw,
270 Route,
271 Save,
272 Screen,
273 SearchCode,
274 SearchSelection,
275 SelectAll,
276 Server,
277 Settings,
278 SettingsAlt,
279 Shift,
280 Slash,
281 SlashSquare,
282 Sliders,
283 SlidersVertical,
284 Snip,
285 Space,
286 Sparkle,
287 SparkleAlt,
288 SparkleFilled,
289 Spinner,
290 Split,
291 SquareDot,
292 SquareMinus,
293 SquarePlus,
294 Star,
295 StarFilled,
296 Stop,
297 Strikethrough,
298 Supermaven,
299 SupermavenDisabled,
300 SupermavenError,
301 SupermavenInit,
302 SwatchBook,
303 Tab,
304 Terminal,
305 TextSnippet,
306 ThumbsUp,
307 ThumbsDown,
308 Trash,
309 TrashAlt,
310 Triangle,
311 TriangleRight,
312 Undo,
313 Unpin,
314 Update,
315 UserGroup,
316 Visible,
317 Wand,
318 Warning,
319 WholeWord,
320 X,
321 XCircle,
322 ZedAssistant,
323 ZedAssistant2,
324 ZedAssistantFilled,
325 ZedPredict,
326 ZedPredictDisabled,
327 ZedXCopilot,
328}
329
330impl From<IconName> for Icon {
331 fn from(icon: IconName) -> Self {
332 Icon::new(icon)
333 }
334}
335
336/// The source of an icon.
337enum IconSource {
338 /// An SVG embedded in the Zed binary.
339 Svg(SharedString),
340 /// An image file located at the specified path.
341 ///
342 /// Currently our SVG renderer is missing support for the following features:
343 /// 1. Loading SVGs from external files.
344 /// 2. Rendering polychrome SVGs.
345 ///
346 /// In order to support icon themes, we render the icons as images instead.
347 Image(Arc<Path>),
348}
349
350impl IconSource {
351 fn from_path(path: impl Into<SharedString>) -> Self {
352 let path = path.into();
353 if path.starts_with("icons/file_icons") {
354 Self::Svg(path)
355 } else {
356 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
357 }
358 }
359}
360
361#[derive(IntoElement)]
362pub struct Icon {
363 source: IconSource,
364 color: Color,
365 size: Rems,
366 transformation: Transformation,
367}
368
369impl Icon {
370 pub fn new(icon: IconName) -> Self {
371 Self {
372 source: IconSource::Svg(icon.path().into()),
373 color: Color::default(),
374 size: IconSize::default().rems(),
375 transformation: Transformation::default(),
376 }
377 }
378
379 pub fn from_path(path: impl Into<SharedString>) -> Self {
380 Self {
381 source: IconSource::from_path(path),
382 color: Color::default(),
383 size: IconSize::default().rems(),
384 transformation: Transformation::default(),
385 }
386 }
387
388 pub fn color(mut self, color: Color) -> Self {
389 self.color = color;
390 self
391 }
392
393 pub fn size(mut self, size: IconSize) -> Self {
394 self.size = size.rems();
395 self
396 }
397
398 /// Sets a custom size for the icon, in [`Rems`].
399 ///
400 /// Not to be exposed outside of the `ui` crate.
401 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
402 self.size = size;
403 self
404 }
405
406 pub fn transform(mut self, transformation: Transformation) -> Self {
407 self.transformation = transformation;
408 self
409 }
410}
411
412impl RenderOnce for Icon {
413 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
414 match self.source {
415 IconSource::Svg(path) => svg()
416 .with_transformation(self.transformation)
417 .size(self.size)
418 .flex_none()
419 .path(path)
420 .text_color(self.color.color(cx))
421 .into_any_element(),
422 IconSource::Image(path) => img(path)
423 .size(self.size)
424 .flex_none()
425 .text_color(self.color.color(cx))
426 .into_any_element(),
427 }
428 }
429}
430
431#[derive(IntoElement)]
432pub struct IconWithIndicator {
433 icon: Icon,
434 indicator: Option<Indicator>,
435 indicator_border_color: Option<Hsla>,
436}
437
438impl IconWithIndicator {
439 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
440 Self {
441 icon,
442 indicator,
443 indicator_border_color: None,
444 }
445 }
446
447 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
448 self.indicator = indicator;
449 self
450 }
451
452 pub fn indicator_color(mut self, color: Color) -> Self {
453 if let Some(indicator) = self.indicator.as_mut() {
454 indicator.color = color;
455 }
456 self
457 }
458
459 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
460 self.indicator_border_color = color;
461 self
462 }
463}
464
465impl RenderOnce for IconWithIndicator {
466 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
467 let indicator_border_color = self
468 .indicator_border_color
469 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
470
471 div()
472 .relative()
473 .child(self.icon)
474 .when_some(self.indicator, |this, indicator| {
475 this.child(
476 div()
477 .absolute()
478 .size_2p5()
479 .border_2()
480 .border_color(indicator_border_color)
481 .rounded_full()
482 .bottom_neg_0p5()
483 .right_neg_0p5()
484 .child(indicator),
485 )
486 })
487 }
488}
489
490impl ComponentPreview for Icon {
491 fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
492 let arrow_icons = vec![
493 IconName::ArrowDown,
494 IconName::ArrowLeft,
495 IconName::ArrowRight,
496 IconName::ArrowUp,
497 IconName::ArrowCircle,
498 ];
499
500 vec![example_group_with_title(
501 "Arrow Icons",
502 arrow_icons
503 .into_iter()
504 .map(|icon| {
505 let name = format!("{:?}", icon).to_string();
506 ComponentExample::new(name, Icon::new(icon))
507 })
508 .collect(),
509 )]
510 }
511}