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