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 AiEdit,
132 AiGoogle,
133 AiLmStudio,
134 AiMistral,
135 AiOllama,
136 AiOpenAi,
137 AiZed,
138 ArrowCircle,
139 ArrowDown,
140 ArrowDownFromLine,
141 ArrowLeft,
142 ArrowRight,
143 ArrowUp,
144 ArrowUpFromLine,
145 ArrowUpRight,
146 AtSign,
147 AudioOff,
148 AudioOn,
149 Backspace,
150 Bell,
151 BellDot,
152 BellOff,
153 BellRing,
154 Blocks,
155 Bolt,
156 Book,
157 BookCopy,
158 BookPlus,
159 CaseSensitive,
160 Check,
161 ChevronDown,
162 /// This chevron indicates a popover menu.
163 ChevronDownSmall,
164 ChevronLeft,
165 ChevronRight,
166 ChevronUp,
167 ChevronUpDown,
168 Circle,
169 Close,
170 Code,
171 Command,
172 Context,
173 Control,
174 Copilot,
175 CopilotDisabled,
176 CopilotError,
177 CopilotInit,
178 Copy,
179 CountdownTimer,
180 CursorIBeam,
181 Dash,
182 DatabaseZap,
183 Delete,
184 Diff,
185 Disconnected,
186 Download,
187 Ellipsis,
188 EllipsisVertical,
189 Envelope,
190 Eraser,
191 Escape,
192 ExpandVertical,
193 Exit,
194 ExternalLink,
195 ExpandUp,
196 ExpandDown,
197 Eye,
198 File,
199 FileCode,
200 FileDoc,
201 FileDiff,
202 FileGeneric,
203 FileGit,
204 FileLock,
205 FileRust,
206 FileSearch,
207 FileText,
208 FileToml,
209 FileTree,
210 Filter,
211 Folder,
212 FolderOpen,
213 FolderX,
214 Font,
215 FontSize,
216 FontWeight,
217 GenericClose,
218 GenericMaximize,
219 GenericMinimize,
220 GenericRestore,
221 Github,
222 Globe,
223 GitBranch,
224 GitBranchSmall,
225 Hash,
226 HistoryRerun,
227 Indicator,
228 Info,
229 InlayHint,
230 Keyboard,
231 Library,
232 LineHeight,
233 Link,
234 ListTree,
235 ListX,
236 LockOutlined,
237 MagnifyingGlass,
238 MailOpen,
239 Maximize,
240 Menu,
241 MessageBubbles,
242 MessageCircle,
243 Cloud,
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 ZedPredictUp,
333 ZedPredictDown,
334 ZedPredictDisabled,
335 ZedPredictError,
336 ZedXCopilot,
337}
338
339impl From<IconName> for Icon {
340 fn from(icon: IconName) -> Self {
341 Icon::new(icon)
342 }
343}
344
345/// The source of an icon.
346enum IconSource {
347 /// An SVG embedded in the Zed binary.
348 Svg(SharedString),
349 /// An image file located at the specified path.
350 ///
351 /// Currently our SVG renderer is missing support for the following features:
352 /// 1. Loading SVGs from external files.
353 /// 2. Rendering polychrome SVGs.
354 ///
355 /// In order to support icon themes, we render the icons as images instead.
356 Image(Arc<Path>),
357}
358
359impl IconSource {
360 fn from_path(path: impl Into<SharedString>) -> Self {
361 let path = path.into();
362 if path.starts_with("icons/file_icons") {
363 Self::Svg(path)
364 } else {
365 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
366 }
367 }
368}
369
370#[derive(IntoElement, IntoComponent)]
371pub struct Icon {
372 source: IconSource,
373 color: Color,
374 size: Rems,
375 transformation: Transformation,
376}
377
378impl Icon {
379 pub fn new(icon: IconName) -> Self {
380 Self {
381 source: IconSource::Svg(icon.path().into()),
382 color: Color::default(),
383 size: IconSize::default().rems(),
384 transformation: Transformation::default(),
385 }
386 }
387
388 pub fn from_path(path: impl Into<SharedString>) -> Self {
389 Self {
390 source: IconSource::from_path(path),
391 color: Color::default(),
392 size: IconSize::default().rems(),
393 transformation: Transformation::default(),
394 }
395 }
396
397 pub fn color(mut self, color: Color) -> Self {
398 self.color = color;
399 self
400 }
401
402 pub fn size(mut self, size: IconSize) -> Self {
403 self.size = size.rems();
404 self
405 }
406
407 /// Sets a custom size for the icon, in [`Rems`].
408 ///
409 /// Not to be exposed outside of the `ui` crate.
410 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
411 self.size = size;
412 self
413 }
414
415 pub fn transform(mut self, transformation: Transformation) -> Self {
416 self.transformation = transformation;
417 self
418 }
419}
420
421impl RenderOnce for Icon {
422 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
423 match self.source {
424 IconSource::Svg(path) => svg()
425 .with_transformation(self.transformation)
426 .size(self.size)
427 .flex_none()
428 .path(path)
429 .text_color(self.color.color(cx))
430 .into_any_element(),
431 IconSource::Image(path) => img(path)
432 .size(self.size)
433 .flex_none()
434 .text_color(self.color.color(cx))
435 .into_any_element(),
436 }
437 }
438}
439
440#[derive(IntoElement)]
441pub struct IconWithIndicator {
442 icon: Icon,
443 indicator: Option<Indicator>,
444 indicator_border_color: Option<Hsla>,
445}
446
447impl IconWithIndicator {
448 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
449 Self {
450 icon,
451 indicator,
452 indicator_border_color: None,
453 }
454 }
455
456 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
457 self.indicator = indicator;
458 self
459 }
460
461 pub fn indicator_color(mut self, color: Color) -> Self {
462 if let Some(indicator) = self.indicator.as_mut() {
463 indicator.color = color;
464 }
465 self
466 }
467
468 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
469 self.indicator_border_color = color;
470 self
471 }
472}
473
474impl RenderOnce for IconWithIndicator {
475 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
476 let indicator_border_color = self
477 .indicator_border_color
478 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
479
480 div()
481 .relative()
482 .child(self.icon)
483 .when_some(self.indicator, |this, indicator| {
484 this.child(
485 div()
486 .absolute()
487 .size_2p5()
488 .border_2()
489 .border_color(indicator_border_color)
490 .rounded_full()
491 .bottom_neg_0p5()
492 .right_neg_0p5()
493 .child(indicator),
494 )
495 })
496 }
497}
498
499// View this component preview using `workspace: open component-preview`
500impl ComponentPreview for Icon {
501 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
502 v_flex()
503 .gap_6()
504 .children(vec![
505 example_group_with_title(
506 "Sizes",
507 vec![
508 single_example("Default", Icon::new(IconName::Star).into_any_element()),
509 single_example(
510 "Small",
511 Icon::new(IconName::Star)
512 .size(IconSize::Small)
513 .into_any_element(),
514 ),
515 single_example(
516 "Large",
517 Icon::new(IconName::Star)
518 .size(IconSize::XLarge)
519 .into_any_element(),
520 ),
521 ],
522 ),
523 example_group_with_title(
524 "Colors",
525 vec![
526 single_example("Default", Icon::new(IconName::Bell).into_any_element()),
527 single_example(
528 "Custom Color",
529 Icon::new(IconName::Bell)
530 .color(Color::Error)
531 .into_any_element(),
532 ),
533 ],
534 ),
535 ])
536 .into_any_element()
537 }
538}