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, AnyElement, Hsla, IntoElement, Rems, Transformation};
11pub use icon_decoration::*;
12use serde::{Deserialize, Serialize};
13use strum::{EnumIter, EnumString, IntoStaticStr};
14use ui_macros::DerivePathStr;
15
16use crate::{prelude::*, Indicator};
17
18#[derive(IntoElement)]
19pub enum AnyIcon {
20 Icon(Icon),
21 AnimatedIcon(AnimationElement<Icon>),
22}
23
24impl AnyIcon {
25 /// Returns a new [`AnyIcon`] after applying the given mapping function
26 /// to the contained [`Icon`].
27 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
28 match self {
29 Self::Icon(icon) => Self::Icon(f(icon)),
30 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
31 }
32 }
33}
34
35impl From<Icon> for AnyIcon {
36 fn from(value: Icon) -> Self {
37 Self::Icon(value)
38 }
39}
40
41impl From<AnimationElement<Icon>> for AnyIcon {
42 fn from(value: AnimationElement<Icon>) -> Self {
43 Self::AnimatedIcon(value)
44 }
45}
46
47impl RenderOnce for AnyIcon {
48 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
49 match self {
50 Self::Icon(icon) => icon.into_any_element(),
51 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
52 }
53 }
54}
55
56#[derive(Default, PartialEq, Copy, Clone)]
57pub enum IconSize {
58 /// 10px
59 Indicator,
60 /// 12px
61 XSmall,
62 /// 14px
63 Small,
64 #[default]
65 /// 16px
66 Medium,
67 /// 48px
68 XLarge,
69 Custom(Rems),
70}
71
72impl IconSize {
73 pub fn rems(self) -> Rems {
74 match self {
75 IconSize::Indicator => rems_from_px(10.),
76 IconSize::XSmall => rems_from_px(12.),
77 IconSize::Small => rems_from_px(14.),
78 IconSize::Medium => rems_from_px(16.),
79 IconSize::XLarge => rems_from_px(48.),
80 IconSize::Custom(size) => size,
81 }
82 }
83
84 /// Returns the individual components of the square that contains this [`IconSize`].
85 ///
86 /// The returned tuple contains:
87 /// 1. The length of one side of the square
88 /// 2. The padding of one side of the square
89 pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) {
90 let icon_size = self.rems() * window.rem_size();
91 let padding = match self {
92 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
93 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
94 IconSize::Small => DynamicSpacing::Base02.px(cx),
95 IconSize::Medium => DynamicSpacing::Base02.px(cx),
96 IconSize::XLarge => DynamicSpacing::Base02.px(cx),
97 // TODO: Wire into dynamic spacing
98 IconSize::Custom(size) => size.to_pixels(window.rem_size()),
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 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 Eye,
196 File,
197 FileCode,
198 FileDoc,
199 FileDiff,
200 FileGeneric,
201 FileGit,
202 FileLock,
203 FileRust,
204 FileSearch,
205 FileText,
206 FileToml,
207 FileTree,
208 Filter,
209 Folder,
210 FolderOpen,
211 FolderX,
212 Font,
213 FontSize,
214 FontWeight,
215 GenericClose,
216 GenericMaximize,
217 GenericMinimize,
218 GenericRestore,
219 Github,
220 Globe,
221 GitBranch,
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: &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}