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 strkethrough 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 CursorText,
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 Snip,
259 Space,
260 Sparkle,
261 SparkleAlt,
262 SparkleFilled,
263 Spinner,
264 Split,
265 Star,
266 StarFilled,
267 Stop,
268 Strikethrough,
269 Supermaven,
270 SupermavenDisabled,
271 SupermavenError,
272 SupermavenInit,
273 Tab,
274 Terminal,
275 Trash,
276 TriangleRight,
277 Undo,
278 Unpin,
279 Update,
280 UserGroup,
281 Visible,
282 Warning,
283 WholeWord,
284 XCircle,
285 ZedAssistant,
286 ZedAssistantFilled,
287}
288
289#[derive(IntoElement)]
290pub struct Icon {
291 path: SharedString,
292 color: Color,
293 size: Rems,
294 transformation: Transformation,
295}
296
297impl Icon {
298 pub fn new(icon: IconName) -> Self {
299 Self {
300 path: icon.path().into(),
301 color: Color::default(),
302 size: IconSize::default().rems(),
303 transformation: Transformation::default(),
304 }
305 }
306
307 pub fn from_path(path: impl Into<SharedString>) -> Self {
308 Self {
309 path: path.into(),
310 color: Color::default(),
311 size: IconSize::default().rems(),
312 transformation: Transformation::default(),
313 }
314 }
315
316 pub fn color(mut self, color: Color) -> Self {
317 self.color = color;
318 self
319 }
320
321 pub fn size(mut self, size: IconSize) -> Self {
322 self.size = size.rems();
323 self
324 }
325
326 /// Sets a custom size for the icon, in [`Rems`].
327 ///
328 /// Not to be exposed outside of the `ui` crate.
329 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
330 self.size = size;
331 self
332 }
333
334 pub fn transform(mut self, transformation: Transformation) -> Self {
335 self.transformation = transformation;
336 self
337 }
338}
339
340impl RenderOnce for Icon {
341 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
342 svg()
343 .with_transformation(self.transformation)
344 .size(self.size)
345 .flex_none()
346 .path(self.path)
347 .text_color(self.color.color(cx))
348 }
349}
350
351#[derive(IntoElement)]
352pub struct DecoratedIcon {
353 icon: Icon,
354 decoration: IconDecoration,
355 decoration_color: Color,
356 parent_background: Option<Hsla>,
357}
358
359impl DecoratedIcon {
360 pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
361 Self {
362 icon,
363 decoration,
364 decoration_color: Color::Default,
365 parent_background: None,
366 }
367 }
368
369 pub fn decoration_color(mut self, color: Color) -> Self {
370 self.decoration_color = color;
371 self
372 }
373
374 pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
375 self.parent_background = background;
376 self
377 }
378}
379
380impl RenderOnce for DecoratedIcon {
381 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
382 let background = self
383 .parent_background
384 .unwrap_or(cx.theme().colors().background);
385
386 let size = self.icon.size;
387
388 let decoration_icon = match self.decoration {
389 IconDecoration::Strikethrough => IconName::Strikethrough,
390 IconDecoration::IndicatorDot => IconName::Indicator,
391 IconDecoration::X => IconName::IndicatorX,
392 };
393
394 let decoration_svg = |icon: IconName| {
395 svg()
396 .absolute()
397 .top_0()
398 .left_0()
399 .path(icon.path())
400 .size(size)
401 .flex_none()
402 .text_color(self.decoration_color.color(cx))
403 };
404
405 let decoration_knockout = |icon: IconName| {
406 svg()
407 .absolute()
408 .top(-rems_from_px(2.))
409 .left(-rems_from_px(3.))
410 .path(icon.path())
411 .size(size + rems_from_px(2.))
412 .flex_none()
413 .text_color(background)
414 };
415
416 div()
417 .relative()
418 .size(self.icon.size)
419 .child(self.icon)
420 .child(decoration_knockout(decoration_icon))
421 .child(decoration_svg(decoration_icon))
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 .w_2()
473 .h_2()
474 .border_1()
475 .border_color(indicator_border_color)
476 .rounded_full()
477 .bottom_neg_0p5()
478 .right_neg_1()
479 .child(indicator),
480 )
481 })
482 }
483}