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 ZedXCopilot,
327}
328
329impl From<IconName> for Icon {
330 fn from(icon: IconName) -> Self {
331 Icon::new(icon)
332 }
333}
334
335/// The source of an icon.
336enum IconSource {
337 /// An SVG embedded in the Zed binary.
338 Svg(SharedString),
339 /// An image file located at the specified path.
340 ///
341 /// Currently our SVG renderer is missing support for the following features:
342 /// 1. Loading SVGs from external files.
343 /// 2. Rendering polychrome SVGs.
344 ///
345 /// In order to support icon themes, we render the icons as images instead.
346 Image(Arc<Path>),
347}
348
349impl IconSource {
350 fn from_path(path: impl Into<SharedString>) -> Self {
351 let path = path.into();
352 if path.starts_with("icons/file_icons") {
353 Self::Svg(path)
354 } else {
355 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
356 }
357 }
358}
359
360#[derive(IntoElement)]
361pub struct Icon {
362 source: IconSource,
363 color: Color,
364 size: Rems,
365 transformation: Transformation,
366}
367
368impl Icon {
369 pub fn new(icon: IconName) -> Self {
370 Self {
371 source: IconSource::Svg(icon.path().into()),
372 color: Color::default(),
373 size: IconSize::default().rems(),
374 transformation: Transformation::default(),
375 }
376 }
377
378 pub fn from_path(path: impl Into<SharedString>) -> Self {
379 Self {
380 source: IconSource::from_path(path),
381 color: Color::default(),
382 size: IconSize::default().rems(),
383 transformation: Transformation::default(),
384 }
385 }
386
387 pub fn color(mut self, color: Color) -> Self {
388 self.color = color;
389 self
390 }
391
392 pub fn size(mut self, size: IconSize) -> Self {
393 self.size = size.rems();
394 self
395 }
396
397 /// Sets a custom size for the icon, in [`Rems`].
398 ///
399 /// Not to be exposed outside of the `ui` crate.
400 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
401 self.size = size;
402 self
403 }
404
405 pub fn transform(mut self, transformation: Transformation) -> Self {
406 self.transformation = transformation;
407 self
408 }
409}
410
411impl RenderOnce for Icon {
412 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
413 match self.source {
414 IconSource::Svg(path) => svg()
415 .with_transformation(self.transformation)
416 .size(self.size)
417 .flex_none()
418 .path(path)
419 .text_color(self.color.color(cx))
420 .into_any_element(),
421 IconSource::Image(path) => img(path)
422 .size(self.size)
423 .flex_none()
424 .text_color(self.color.color(cx))
425 .into_any_element(),
426 }
427 }
428}
429
430#[derive(IntoElement)]
431pub struct IconWithIndicator {
432 icon: Icon,
433 indicator: Option<Indicator>,
434 indicator_border_color: Option<Hsla>,
435}
436
437impl IconWithIndicator {
438 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
439 Self {
440 icon,
441 indicator,
442 indicator_border_color: None,
443 }
444 }
445
446 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
447 self.indicator = indicator;
448 self
449 }
450
451 pub fn indicator_color(mut self, color: Color) -> Self {
452 if let Some(indicator) = self.indicator.as_mut() {
453 indicator.color = color;
454 }
455 self
456 }
457
458 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
459 self.indicator_border_color = color;
460 self
461 }
462}
463
464impl RenderOnce for IconWithIndicator {
465 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
466 let indicator_border_color = self
467 .indicator_border_color
468 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
469
470 div()
471 .relative()
472 .child(self.icon)
473 .when_some(self.indicator, |this, indicator| {
474 this.child(
475 div()
476 .absolute()
477 .size_2p5()
478 .border_2()
479 .border_color(indicator_border_color)
480 .rounded_full()
481 .bottom_neg_0p5()
482 .right_neg_0p5()
483 .child(indicator),
484 )
485 })
486 }
487}
488
489impl ComponentPreview for Icon {
490 fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
491 let arrow_icons = vec![
492 IconName::ArrowDown,
493 IconName::ArrowLeft,
494 IconName::ArrowRight,
495 IconName::ArrowUp,
496 IconName::ArrowCircle,
497 ];
498
499 vec![example_group_with_title(
500 "Arrow Icons",
501 arrow_icons
502 .into_iter()
503 .map(|icon| {
504 let name = format!("{:?}", icon).to_string();
505 ComponentExample::new(name, Icon::new(icon))
506 })
507 .collect(),
508 )]
509 }
510}