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 TrashAlt,
279 TriangleRight,
280 Undo,
281 Unpin,
282 Update,
283 UserGroup,
284 Visible,
285 Warning,
286 WholeWord,
287 XCircle,
288 ZedAssistant,
289 ZedAssistantFilled,
290 ZedXCopilot,
291}
292
293#[derive(IntoElement)]
294pub struct Icon {
295 path: SharedString,
296 color: Color,
297 size: Rems,
298 transformation: Transformation,
299}
300
301impl Icon {
302 pub fn new(icon: IconName) -> Self {
303 Self {
304 path: icon.path().into(),
305 color: Color::default(),
306 size: IconSize::default().rems(),
307 transformation: Transformation::default(),
308 }
309 }
310
311 pub fn from_path(path: impl Into<SharedString>) -> Self {
312 Self {
313 path: path.into(),
314 color: Color::default(),
315 size: IconSize::default().rems(),
316 transformation: Transformation::default(),
317 }
318 }
319
320 pub fn color(mut self, color: Color) -> Self {
321 self.color = color;
322 self
323 }
324
325 pub fn size(mut self, size: IconSize) -> Self {
326 self.size = size.rems();
327 self
328 }
329
330 /// Sets a custom size for the icon, in [`Rems`].
331 ///
332 /// Not to be exposed outside of the `ui` crate.
333 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
334 self.size = size;
335 self
336 }
337
338 pub fn transform(mut self, transformation: Transformation) -> Self {
339 self.transformation = transformation;
340 self
341 }
342}
343
344impl RenderOnce for Icon {
345 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
346 svg()
347 .with_transformation(self.transformation)
348 .size(self.size)
349 .flex_none()
350 .path(self.path)
351 .text_color(self.color.color(cx))
352 }
353}
354
355#[derive(IntoElement)]
356pub struct DecoratedIcon {
357 icon: Icon,
358 decoration: IconDecoration,
359 decoration_color: Color,
360 parent_background: Option<Hsla>,
361}
362
363impl DecoratedIcon {
364 pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
365 Self {
366 icon,
367 decoration,
368 decoration_color: Color::Default,
369 parent_background: None,
370 }
371 }
372
373 pub fn decoration_color(mut self, color: Color) -> Self {
374 self.decoration_color = color;
375 self
376 }
377
378 pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
379 self.parent_background = background;
380 self
381 }
382}
383
384impl RenderOnce for DecoratedIcon {
385 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
386 let background = self
387 .parent_background
388 .unwrap_or(cx.theme().colors().background);
389
390 let size = self.icon.size;
391
392 let decoration_icon = match self.decoration {
393 IconDecoration::Strikethrough => IconName::Strikethrough,
394 IconDecoration::IndicatorDot => IconName::Indicator,
395 IconDecoration::X => IconName::IndicatorX,
396 };
397
398 let decoration_svg = |icon: IconName| {
399 svg()
400 .absolute()
401 .top_0()
402 .left_0()
403 .path(icon.path())
404 .size(size)
405 .flex_none()
406 .text_color(self.decoration_color.color(cx))
407 };
408
409 let decoration_knockout = |icon: IconName| {
410 svg()
411 .absolute()
412 .top(-rems_from_px(2.))
413 .left(-rems_from_px(3.))
414 .path(icon.path())
415 .size(size + rems_from_px(2.))
416 .flex_none()
417 .text_color(background)
418 };
419
420 div()
421 .relative()
422 .size(self.icon.size)
423 .child(self.icon)
424 .child(decoration_knockout(decoration_icon))
425 .child(decoration_svg(decoration_icon))
426 }
427}
428
429#[derive(IntoElement)]
430pub struct IconWithIndicator {
431 icon: Icon,
432 indicator: Option<Indicator>,
433 indicator_border_color: Option<Hsla>,
434}
435
436impl IconWithIndicator {
437 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
438 Self {
439 icon,
440 indicator,
441 indicator_border_color: None,
442 }
443 }
444
445 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
446 self.indicator = indicator;
447 self
448 }
449
450 pub fn indicator_color(mut self, color: Color) -> Self {
451 if let Some(indicator) = self.indicator.as_mut() {
452 indicator.color = color;
453 }
454 self
455 }
456
457 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
458 self.indicator_border_color = color;
459 self
460 }
461}
462
463impl RenderOnce for IconWithIndicator {
464 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
465 let indicator_border_color = self
466 .indicator_border_color
467 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
468
469 div()
470 .relative()
471 .child(self.icon)
472 .when_some(self.indicator, |this, indicator| {
473 this.child(
474 div()
475 .absolute()
476 .w_2()
477 .h_2()
478 .border_1()
479 .border_color(indicator_border_color)
480 .rounded_full()
481 .bottom_neg_0p5()
482 .right_neg_1()
483 .child(indicator),
484 )
485 })
486 }
487}