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, Hsla, IntoElement, Rems, Transformation};
11pub use icon_decoration::*;
12use serde::{Deserialize, Serialize};
13use strum::{EnumIter, EnumString, IntoStaticStr};
14use ui_macros::DerivePathStr;
15
16use crate::{
17 prelude::*,
18 traits::component_preview::{ComponentExample, ComponentPreview},
19 Indicator,
20};
21
22#[derive(IntoElement)]
23pub enum AnyIcon {
24 Icon(Icon),
25 AnimatedIcon(AnimationElement<Icon>),
26}
27
28impl AnyIcon {
29 /// Returns a new [`AnyIcon`] after applying the given mapping function
30 /// to the contained [`Icon`].
31 pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self {
32 match self {
33 Self::Icon(icon) => Self::Icon(f(icon)),
34 Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)),
35 }
36 }
37}
38
39impl From<Icon> for AnyIcon {
40 fn from(value: Icon) -> Self {
41 Self::Icon(value)
42 }
43}
44
45impl From<AnimationElement<Icon>> for AnyIcon {
46 fn from(value: AnimationElement<Icon>) -> Self {
47 Self::AnimatedIcon(value)
48 }
49}
50
51impl RenderOnce for AnyIcon {
52 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
53 match self {
54 Self::Icon(icon) => icon.into_any_element(),
55 Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(),
56 }
57 }
58}
59
60#[derive(Default, PartialEq, Copy, Clone)]
61pub enum IconSize {
62 /// 10px
63 Indicator,
64 /// 12px
65 XSmall,
66 /// 14px
67 Small,
68 #[default]
69 /// 16px
70 Medium,
71}
72
73impl IconSize {
74 pub fn rems(self) -> Rems {
75 match self {
76 IconSize::Indicator => rems_from_px(10.),
77 IconSize::XSmall => rems_from_px(12.),
78 IconSize::Small => rems_from_px(14.),
79 IconSize::Medium => rems_from_px(16.),
80 }
81 }
82
83 /// Returns the individual components of the square that contains this [`IconSize`].
84 ///
85 /// The returned tuple contains:
86 /// 1. The length of one side of the square
87 /// 2. The padding of one side of the square
88 pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
89 let icon_size = self.rems() * cx.rem_size();
90 let padding = match self {
91 IconSize::Indicator => DynamicSpacing::Base00.px(cx),
92 IconSize::XSmall => DynamicSpacing::Base02.px(cx),
93 IconSize::Small => DynamicSpacing::Base02.px(cx),
94 IconSize::Medium => DynamicSpacing::Base02.px(cx),
95 };
96
97 (icon_size, padding)
98 }
99
100 /// Returns the length of a side of the square that contains this [`IconSize`], with padding.
101 pub fn square(&self, cx: &mut WindowContext) -> Pixels {
102 let (icon_size, padding) = self.square_components(cx);
103
104 icon_size + padding * 2.
105 }
106}
107
108#[derive(
109 Debug,
110 PartialEq,
111 Eq,
112 Copy,
113 Clone,
114 EnumIter,
115 EnumString,
116 IntoStaticStr,
117 Serialize,
118 Deserialize,
119 DerivePathStr,
120)]
121#[strum(serialize_all = "snake_case")]
122#[path_str(prefix = "icons", suffix = ".svg")]
123pub enum IconName {
124 Ai,
125 AiAnthropic,
126 AiAnthropicHosted,
127 AiGoogle,
128 AiLmStudio,
129 AiOllama,
130 AiOpenAi,
131 AiZed,
132 ArrowCircle,
133 ArrowDown,
134 ArrowDownFromLine,
135 ArrowLeft,
136 ArrowRight,
137 ArrowUp,
138 ArrowUpFromLine,
139 ArrowUpRight,
140 AtSign,
141 AudioOff,
142 AudioOn,
143 Backspace,
144 Bell,
145 BellDot,
146 BellOff,
147 BellRing,
148 Blocks,
149 Bolt,
150 Book,
151 BookCopy,
152 BookPlus,
153 CaseSensitive,
154 Check,
155 ChevronDown,
156 /// This chevron indicates a popover menu.
157 ChevronDownSmall,
158 ChevronLeft,
159 ChevronRight,
160 ChevronUp,
161 ChevronUpDown,
162 Close,
163 Code,
164 Command,
165 Context,
166 Control,
167 Copilot,
168 CopilotDisabled,
169 CopilotError,
170 CopilotInit,
171 Copy,
172 CountdownTimer,
173 CursorIBeam,
174 Dash,
175 DatabaseZap,
176 Delete,
177 Diff,
178 Disconnected,
179 Download,
180 Ellipsis,
181 EllipsisVertical,
182 Envelope,
183 Eraser,
184 Escape,
185 ExpandVertical,
186 Exit,
187 ExternalLink,
188 Eye,
189 File,
190 FileCode,
191 FileDoc,
192 FileDiff,
193 FileGeneric,
194 FileGit,
195 FileLock,
196 FileRust,
197 FileSearch,
198 FileText,
199 FileToml,
200 FileTree,
201 Filter,
202 Folder,
203 FolderOpen,
204 FolderX,
205 Font,
206 FontSize,
207 FontWeight,
208 GenericClose,
209 GenericMaximize,
210 GenericMinimize,
211 GenericRestore,
212 Github,
213 Globe,
214 GitBranch,
215 Hash,
216 HistoryRerun,
217 Indicator,
218 IndicatorX,
219 Info,
220 InlayHint,
221 Keyboard,
222 Library,
223 LineHeight,
224 Link,
225 ListTree,
226 ListX,
227 MagnifyingGlass,
228 MailOpen,
229 Maximize,
230 Menu,
231 MessageBubbles,
232 MessageCircle,
233 Mic,
234 MicMute,
235 Microscope,
236 Minimize,
237 Option,
238 PageDown,
239 PageUp,
240 PanelLeft,
241 PanelRight,
242 Pencil,
243 Person,
244 PersonCircle,
245 PhoneIncoming,
246 Pin,
247 Play,
248 Plus,
249 PocketKnife,
250 Public,
251 PullRequest,
252 Quote,
253 RefreshTitle,
254 Regex,
255 ReplNeutral,
256 Replace,
257 ReplaceAll,
258 ReplaceNext,
259 ReplyArrowRight,
260 Rerun,
261 Return,
262 Reveal,
263 RotateCcw,
264 RotateCw,
265 Route,
266 Save,
267 Screen,
268 SearchCode,
269 SearchSelection,
270 SelectAll,
271 Server,
272 Settings,
273 SettingsAlt,
274 Shift,
275 Slash,
276 SlashSquare,
277 Sliders,
278 SlidersVertical,
279 Snip,
280 Space,
281 Sparkle,
282 SparkleAlt,
283 SparkleFilled,
284 Spinner,
285 Split,
286 SquareDot,
287 SquareMinus,
288 SquarePlus,
289 Star,
290 StarFilled,
291 Stop,
292 Strikethrough,
293 Supermaven,
294 SupermavenDisabled,
295 SupermavenError,
296 SupermavenInit,
297 SwatchBook,
298 Tab,
299 Terminal,
300 TextSnippet,
301 ThumbsUp,
302 ThumbsDown,
303 Trash,
304 TrashAlt,
305 Triangle,
306 TriangleRight,
307 Undo,
308 Unpin,
309 Update,
310 UserGroup,
311 Visible,
312 Wand,
313 Warning,
314 WholeWord,
315 X,
316 XCircle,
317 ZedAssistant,
318 ZedAssistant2,
319 ZedAssistantFilled,
320 ZedPredict,
321 ZedXCopilot,
322}
323
324impl From<IconName> for Icon {
325 fn from(icon: IconName) -> Self {
326 Icon::new(icon)
327 }
328}
329
330/// The source of an icon.
331enum IconSource {
332 /// An SVG embedded in the Zed binary.
333 Svg(SharedString),
334 /// An image file located at the specified path.
335 ///
336 /// Currently our SVG renderer is missing support for the following features:
337 /// 1. Loading SVGs from external files.
338 /// 2. Rendering polychrome SVGs.
339 ///
340 /// In order to support icon themes, we render the icons as images instead.
341 Image(Arc<Path>),
342}
343
344impl IconSource {
345 fn from_path(path: impl Into<SharedString>) -> Self {
346 let path = path.into();
347 if path.starts_with("icons/file_icons") {
348 Self::Svg(path)
349 } else {
350 Self::Image(Arc::from(PathBuf::from(path.as_ref())))
351 }
352 }
353}
354
355#[derive(IntoElement)]
356pub struct Icon {
357 source: IconSource,
358 color: Color,
359 size: Rems,
360 transformation: Transformation,
361}
362
363impl Icon {
364 pub fn new(icon: IconName) -> Self {
365 Self {
366 source: IconSource::Svg(icon.path().into()),
367 color: Color::default(),
368 size: IconSize::default().rems(),
369 transformation: Transformation::default(),
370 }
371 }
372
373 pub fn from_path(path: impl Into<SharedString>) -> Self {
374 Self {
375 source: IconSource::from_path(path),
376 color: Color::default(),
377 size: IconSize::default().rems(),
378 transformation: Transformation::default(),
379 }
380 }
381
382 pub fn color(mut self, color: Color) -> Self {
383 self.color = color;
384 self
385 }
386
387 pub fn size(mut self, size: IconSize) -> Self {
388 self.size = size.rems();
389 self
390 }
391
392 /// Sets a custom size for the icon, in [`Rems`].
393 ///
394 /// Not to be exposed outside of the `ui` crate.
395 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
396 self.size = size;
397 self
398 }
399
400 pub fn transform(mut self, transformation: Transformation) -> Self {
401 self.transformation = transformation;
402 self
403 }
404}
405
406impl RenderOnce for Icon {
407 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
408 match self.source {
409 IconSource::Svg(path) => svg()
410 .with_transformation(self.transformation)
411 .size(self.size)
412 .flex_none()
413 .path(path)
414 .text_color(self.color.color(cx))
415 .into_any_element(),
416 IconSource::Image(path) => img(path)
417 .size(self.size)
418 .flex_none()
419 .text_color(self.color.color(cx))
420 .into_any_element(),
421 }
422 }
423}
424
425#[derive(IntoElement)]
426pub struct IconWithIndicator {
427 icon: Icon,
428 indicator: Option<Indicator>,
429 indicator_border_color: Option<Hsla>,
430}
431
432impl IconWithIndicator {
433 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
434 Self {
435 icon,
436 indicator,
437 indicator_border_color: None,
438 }
439 }
440
441 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
442 self.indicator = indicator;
443 self
444 }
445
446 pub fn indicator_color(mut self, color: Color) -> Self {
447 if let Some(indicator) = self.indicator.as_mut() {
448 indicator.color = color;
449 }
450 self
451 }
452
453 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
454 self.indicator_border_color = color;
455 self
456 }
457}
458
459impl RenderOnce for IconWithIndicator {
460 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
461 let indicator_border_color = self
462 .indicator_border_color
463 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
464
465 div()
466 .relative()
467 .child(self.icon)
468 .when_some(self.indicator, |this, indicator| {
469 this.child(
470 div()
471 .absolute()
472 .size_2p5()
473 .border_2()
474 .border_color(indicator_border_color)
475 .rounded_full()
476 .bottom_neg_0p5()
477 .right_neg_0p5()
478 .child(indicator),
479 )
480 })
481 }
482}
483
484impl ComponentPreview for Icon {
485 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Icon>> {
486 let arrow_icons = vec![
487 IconName::ArrowDown,
488 IconName::ArrowLeft,
489 IconName::ArrowRight,
490 IconName::ArrowUp,
491 IconName::ArrowCircle,
492 ];
493
494 vec![example_group_with_title(
495 "Arrow Icons",
496 arrow_icons
497 .into_iter()
498 .map(|icon| {
499 let name = format!("{:?}", icon).to_string();
500 ComponentExample::new(name, Icon::new(icon))
501 })
502 .collect(),
503 )]
504 }
505}