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