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 Diff,
174 Disconnected,
175 Download,
176 Ellipsis,
177 EllipsisVertical,
178 Envelope,
179 Escape,
180 Exit,
181 ExpandVertical,
182 ExternalLink,
183 Eye,
184 File,
185 FileCode,
186 FileDoc,
187 FileGeneric,
188 FileGit,
189 FileLock,
190 FileRust,
191 FileText,
192 FileToml,
193 FileTree,
194 Filter,
195 Folder,
196 FolderOpen,
197 FolderX,
198 Font,
199 FontSize,
200 FontWeight,
201 GenericClose,
202 GenericMaximize,
203 GenericMinimize,
204 GenericRestore,
205 Github,
206 Hash,
207 HistoryRerun,
208 Indicator,
209 IndicatorX,
210 InlayHint,
211 Library,
212 LineHeight,
213 Link,
214 ListTree,
215 MagnifyingGlass,
216 MailOpen,
217 Maximize,
218 Menu,
219 MessageBubbles,
220 Mic,
221 MicMute,
222 Microscope,
223 Minimize,
224 Option,
225 PageDown,
226 PageUp,
227 Pencil,
228 Person,
229 Pin,
230 Play,
231 Plus,
232 PocketKnife,
233 Public,
234 PullRequest,
235 Quote,
236 Regex,
237 ReplNeutral,
238 Replace,
239 ReplaceAll,
240 ReplaceNext,
241 ReplyArrowRight,
242 Rerun,
243 Return,
244 Reveal,
245 RotateCcw,
246 RotateCw,
247 Route,
248 Save,
249 Screen,
250 SearchCode,
251 SearchSelection,
252 SelectAll,
253 Server,
254 Settings,
255 SettingsAlt,
256 Shift,
257 Slash,
258 SlashSquare,
259 Sliders,
260 SlidersVertical,
261 Snip,
262 Space,
263 Sparkle,
264 SparkleAlt,
265 SparkleFilled,
266 Spinner,
267 Split,
268 Star,
269 StarFilled,
270 Stop,
271 Strikethrough,
272 Supermaven,
273 SupermavenDisabled,
274 SupermavenError,
275 SupermavenInit,
276 Tab,
277 Terminal,
278 Trash,
279 TrashAlt,
280 TriangleRight,
281 Undo,
282 Unpin,
283 Update,
284 UserGroup,
285 Visible,
286 Warning,
287 WholeWord,
288 XCircle,
289 ZedAssistant,
290 ZedAssistantFilled,
291 ZedXCopilot,
292}
293
294#[derive(IntoElement)]
295pub struct Icon {
296 path: SharedString,
297 color: Color,
298 size: Rems,
299 transformation: Transformation,
300}
301
302impl Icon {
303 pub fn new(icon: IconName) -> Self {
304 Self {
305 path: icon.path().into(),
306 color: Color::default(),
307 size: IconSize::default().rems(),
308 transformation: Transformation::default(),
309 }
310 }
311
312 pub fn from_path(path: impl Into<SharedString>) -> Self {
313 Self {
314 path: path.into(),
315 color: Color::default(),
316 size: IconSize::default().rems(),
317 transformation: Transformation::default(),
318 }
319 }
320
321 pub fn color(mut self, color: Color) -> Self {
322 self.color = color;
323 self
324 }
325
326 pub fn size(mut self, size: IconSize) -> Self {
327 self.size = size.rems();
328 self
329 }
330
331 /// Sets a custom size for the icon, in [`Rems`].
332 ///
333 /// Not to be exposed outside of the `ui` crate.
334 pub(crate) fn custom_size(mut self, size: Rems) -> Self {
335 self.size = size;
336 self
337 }
338
339 pub fn transform(mut self, transformation: Transformation) -> Self {
340 self.transformation = transformation;
341 self
342 }
343}
344
345impl RenderOnce for Icon {
346 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
347 svg()
348 .with_transformation(self.transformation)
349 .size(self.size)
350 .flex_none()
351 .path(self.path)
352 .text_color(self.color.color(cx))
353 }
354}
355
356#[derive(IntoElement)]
357pub struct DecoratedIcon {
358 icon: Icon,
359 decoration: IconDecoration,
360 decoration_color: Color,
361 parent_background: Option<Hsla>,
362}
363
364impl DecoratedIcon {
365 pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
366 Self {
367 icon,
368 decoration,
369 decoration_color: Color::Default,
370 parent_background: None,
371 }
372 }
373
374 pub fn decoration_color(mut self, color: Color) -> Self {
375 self.decoration_color = color;
376 self
377 }
378
379 pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
380 self.parent_background = background;
381 self
382 }
383}
384
385impl RenderOnce for DecoratedIcon {
386 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
387 let background = self
388 .parent_background
389 .unwrap_or(cx.theme().colors().background);
390
391 let size = self.icon.size;
392
393 let decoration_icon = match self.decoration {
394 IconDecoration::Strikethrough => IconName::Strikethrough,
395 IconDecoration::IndicatorDot => IconName::Indicator,
396 IconDecoration::X => IconName::IndicatorX,
397 };
398
399 let decoration_svg = |icon: IconName| {
400 svg()
401 .absolute()
402 .top_0()
403 .left_0()
404 .path(icon.path())
405 .size(size)
406 .flex_none()
407 .text_color(self.decoration_color.color(cx))
408 };
409
410 let decoration_knockout = |icon: IconName| {
411 svg()
412 .absolute()
413 .top(-rems_from_px(2.))
414 .left(-rems_from_px(3.))
415 .path(icon.path())
416 .size(size + rems_from_px(2.))
417 .flex_none()
418 .text_color(background)
419 };
420
421 div()
422 .relative()
423 .size(self.icon.size)
424 .child(self.icon)
425 .child(decoration_knockout(decoration_icon))
426 .child(decoration_svg(decoration_icon))
427 }
428}
429
430#[derive(IntoElement)]
431pub struct IconWithIndicator {
432 icon: Icon,
433 indicator: Option<Indicator>,
434 indicator_border_color: Option<Hsla>,
435}
436
437impl IconWithIndicator {
438 pub fn new(icon: Icon, indicator: Option<Indicator>) -> Self {
439 Self {
440 icon,
441 indicator,
442 indicator_border_color: None,
443 }
444 }
445
446 pub fn indicator(mut self, indicator: Option<Indicator>) -> Self {
447 self.indicator = indicator;
448 self
449 }
450
451 pub fn indicator_color(mut self, color: Color) -> Self {
452 if let Some(indicator) = self.indicator.as_mut() {
453 indicator.color = color;
454 }
455 self
456 }
457
458 pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
459 self.indicator_border_color = color;
460 self
461 }
462}
463
464impl RenderOnce for IconWithIndicator {
465 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
466 let indicator_border_color = self
467 .indicator_border_color
468 .unwrap_or_else(|| cx.theme().colors().elevated_surface_background);
469
470 div()
471 .relative()
472 .child(self.icon)
473 .when_some(self.indicator, |this, indicator| {
474 this.child(
475 div()
476 .absolute()
477 .size_2p5()
478 .border_2()
479 .border_color(indicator_border_color)
480 .rounded_full()
481 .bottom_neg_0p5()
482 .right_neg_0p5()
483 .child(indicator),
484 )
485 })
486 }
487}