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 AiBedrock,
129 AiAnthropicHosted,
130 AiDeepSeek,
131 AiGoogle,
132 AiLmStudio,
133 AiMistral,
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 GitBranchSmall,
222 Hash,
223 HistoryRerun,
224 Indicator,
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 Cloud,
241 Mic,
242 MicMute,
243 Microscope,
244 Minimize,
245 Option,
246 PageDown,
247 PageUp,
248 PanelLeft,
249 PanelRight,
250 Pencil,
251 Person,
252 PersonCircle,
253 PhoneIncoming,
254 Pin,
255 Play,
256 Plus,
257 PocketKnife,
258 Public,
259 PullRequest,
260 Quote,
261 RefreshTitle,
262 Regex,
263 ReplNeutral,
264 Replace,
265 ReplaceAll,
266 ReplaceNext,
267 ReplyArrowRight,
268 Rerun,
269 Return,
270 Reveal,
271 RotateCcw,
272 RotateCw,
273 Route,
274 Save,
275 Screen,
276 SearchCode,
277 SearchSelection,
278 SelectAll,
279 Server,
280 Settings,
281 SettingsAlt,
282 Shift,
283 Slash,
284 SlashSquare,
285 Sliders,
286 SlidersVertical,
287 Snip,
288 Space,
289 Sparkle,
290 SparkleAlt,
291 SparkleFilled,
292 Spinner,
293 Split,
294 SquareDot,
295 SquareMinus,
296 SquarePlus,
297 Star,
298 StarFilled,
299 Stop,
300 Strikethrough,
301 Supermaven,
302 SupermavenDisabled,
303 SupermavenError,
304 SupermavenInit,
305 SwatchBook,
306 Tab,
307 Terminal,
308 TextSnippet,
309 ThumbsUp,
310 ThumbsDown,
311 Trash,
312 TrashAlt,
313 Triangle,
314 TriangleRight,
315 Undo,
316 Unpin,
317 Update,
318 UserGroup,
319 Visible,
320 Wand,
321 Warning,
322 WholeWord,
323 X,
324 XCircle,
325 ZedAssistant,
326 ZedAssistant2,
327 ZedAssistantFilled,
328 ZedPredict,
329 ZedPredictUp,
330 ZedPredictDown,
331 ZedPredictDisabled,
332 ZedXCopilot,
333}
334
335impl From<IconName> for Icon {
336 fn from(icon: IconName) -> Self {
337 Icon::new(icon)
338 }
339}
340
341/// The source of an icon.
342enum IconSource {
343 /// An SVG embedded in the Zed binary.
344 Svg(SharedString),
345 /// An image file located at the specified path.
346 ///
347 /// Currently our SVG renderer is missing support for the following features:
348 /// 1. Loading SVGs from external files.
349 /// 2. Rendering polychrome SVGs.
350 ///
351 /// In order to support icon themes, we render the icons as images instead.
352 Image(Arc<Path>),
353}
354
355impl IconSource {
356 fn from_path(path: impl Into<SharedString>) -> Self {
357 let path = path.into();
358 if path.starts_with("icons/file_icons") {
359 Self::Svg(path)
360 } else {
361 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
362 }
363 }
364}
365
366#[derive(IntoElement, IntoComponent)]
367pub struct Icon {
368 source: IconSource,
369 color: Color,
370 size: Rems,
371 transformation: Transformation,
372}
373
374impl Icon {
375 pub fn new(icon: IconName) -> Self {
376 Self {
377 source: IconSource::Svg(icon.path().into()),
378 color: Color::default(),
379 size: IconSize::default().rems(),
380 transformation: Transformation::default(),
381 }
382 }
383
384 pub fn from_path(path: impl Into<SharedString>) -> Self {
385 Self {
386 source: IconSource::from_path(path),
387 color: Color::default(),
388 size: IconSize::default().rems(),
389 transformation: Transformation::default(),
390 }
391 }
392
393 pub fn color(mut self, color: Color) -> Self {
394 self.color = color;
395 self
396 }
397
398 pub fn size(mut self, size: IconSize) -> Self {
399 self.size = size.rems();
400 self
401 }
402
403 /// Sets a custom size for the icon, in [`Rems`].
404 ///
405 /// Not to be exposed outside of the `ui` crate.
406 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
407 self.size = size;
408 self
409 }
410
411 pub fn transform(mut self, transformation: Transformation) -> Self {
412 self.transformation = transformation;
413 self
414 }
415}
416
417impl RenderOnce for Icon {
418 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
419 match self.source {
420 IconSource::Svg(path) => svg()
421 .with_transformation(self.transformation)
422 .size(self.size)
423 .flex_none()
424 .path(path)
425 .text_color(self.color.color(cx))
426 .into_any_element(),
427 IconSource::Image(path) => img(path)
428 .size(self.size)
429 .flex_none()
430 .text_color(self.color.color(cx))
431 .into_any_element(),
432 }
433 }
434}
435
436#[derive(IntoElement)]
437pub struct IconWithIndicator {
438 icon: Icon,
439 indicator: Option<Indicator>,
440 indicator_border_color: Option<Hsla>,
441}
442
443impl IconWithIndicator {
444 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
445 Self {
446 icon,
447 indicator,
448 indicator_border_color: None,
449 }
450 }
451
452 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
453 self.indicator = indicator;
454 self
455 }
456
457 pub fn indicator_color(mut self, color: Color) -> Self {
458 if let Some(indicator) = self.indicator.as_mut() {
459 indicator.color = color;
460 }
461 self
462 }
463
464 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
465 self.indicator_border_color = color;
466 self
467 }
468}
469
470impl RenderOnce for IconWithIndicator {
471 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
472 let indicator_border_color = self
473 .indicator_border_color
474 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
475
476 div()
477 .relative()
478 .child(self.icon)
479 .when_some(self.indicator, |this, indicator| {
480 this.child(
481 div()
482 .absolute()
483 .size_2p5()
484 .border_2()
485 .border_color(indicator_border_color)
486 .rounded_full()
487 .bottom_neg_0p5()
488 .right_neg_0p5()
489 .child(indicator),
490 )
491 })
492 }
493}
494
495// View this component preview using `workspace: open component-preview`
496impl ComponentPreview for Icon {
497 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
498 v_flex()
499 .gap_6()
500 .children(vec![
501 example_group_with_title(
502 "Sizes",
503 vec![
504 single_example("Default", Icon::new(IconName::Star).into_any_element()),
505 single_example(
506 "Small",
507 Icon::new(IconName::Star)
508 .size(IconSize::Small)
509 .into_any_element(),
510 ),
511 single_example(
512 "Large",
513 Icon::new(IconName::Star)
514 .size(IconSize::XLarge)
515 .into_any_element(),
516 ),
517 ],
518 ),
519 example_group_with_title(
520 "Colors",
521 vec![
522 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
523 single_example(
524 "Custom Color",
525 Icon::new(IconName::Bell)
526 .color(Color::Error)
527 .into_any_element(),
528 ),
529 ],
530 ),
531 ])
532 .into_any_element()
533 }
534}