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