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