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 AiAnthropicHosted,
129 AiDeepSeek,
130 AiGoogle,
131 AiLmStudio,
132 AiMistral,
133 AiOllama,
134 AiOpenAi,
135 AiZed,
136 ArrowCircle,
137 ArrowDown,
138 ArrowDownFromLine,
139 ArrowLeft,
140 ArrowRight,
141 ArrowUp,
142 ArrowUpFromLine,
143 ArrowUpRight,
144 AtSign,
145 AudioOff,
146 AudioOn,
147 Backspace,
148 Bell,
149 BellDot,
150 BellOff,
151 BellRing,
152 Blocks,
153 Bolt,
154 Book,
155 BookCopy,
156 BookPlus,
157 CaseSensitive,
158 Check,
159 ChevronDown,
160 /// This chevron indicates a popover menu.
161 ChevronDownSmall,
162 ChevronLeft,
163 ChevronRight,
164 ChevronUp,
165 ChevronUpDown,
166 Circle,
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 Info,
224 InlayHint,
225 Keyboard,
226 Library,
227 LineHeight,
228 Link,
229 ListTree,
230 ListX,
231 LockOutlined,
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 ZedPredictUp,
327 ZedPredictDown,
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
492// View this component preview using `workspace: open component-preview`
493impl ComponentPreview for Icon {
494 fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
495 v_flex()
496 .gap_6()
497 .children(vec![
498 example_group_with_title(
499 "Sizes",
500 vec![
501 single_example("Default", Icon::new(IconName::Star).into_any_element()),
502 single_example(
503 "Small",
504 Icon::new(IconName::Star)
505 .size(IconSize::Small)
506 .into_any_element(),
507 ),
508 single_example(
509 "Large",
510 Icon::new(IconName::Star)
511 .size(IconSize::XLarge)
512 .into_any_element(),
513 ),
514 ],
515 ),
516 example_group_with_title(
517 "Colors",
518 vec![
519 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
520 single_example(
521 "Custom Color",
522 Icon::new(IconName::Bell)
523 .color(Color::Error)
524 .into_any_element(),
525 ),
526 ],
527 ),
528 ])
529 .into_any_element()
530 }
531}