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