1use std::sync::Arc;
2
3use crate::{ActiveTheme, Sizable, Size};
4use gpui::{
5 AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Pixels, Radians, Render,
6 RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
7 prelude::FluentBuilder as _, svg,
8};
9pub use gpui_kit_assets::IconNamed;
10
11macro_rules! component_icon_names {
14 ($($name:ident => $path:literal,)*) => {
15 #[derive(Clone, IntoElement)]
18 pub enum IconName {
19 $($name,)*
20 }
21
22 impl From<IconName> for gpui_kit_assets::IconName {
23 fn from(name: IconName) -> Self {
24 match name {
25 $(IconName::$name => Self::$name,)*
26 }
27 }
28 }
29
30 impl IconNamed for IconName {
31 fn path(self) -> SharedString {
32 match self { $(Self::$name => $path,)* }.into()
33 }
34 }
35 };
36}
37
38gpui_kit_assets::__component_icon_names!(component_icon_names);
39
40impl IconName {
41 pub fn view(self, cx: &mut App) -> Entity<Icon> {
43 Icon::build(self).view(cx)
44 }
45}
46
47impl From<IconName> for AnyElement {
48 fn from(name: IconName) -> Self {
49 Icon::build(name).into_any_element()
50 }
51}
52
53impl RenderOnce for IconName {
54 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
55 Icon::build(self)
56 }
57}
58
59impl<T: IconNamed> From<T> for Icon {
60 fn from(value: T) -> Self {
61 Icon::build(value)
62 }
63}
64
65pub trait IconNameExt {
68 fn view(self, cx: &mut App) -> Entity<Icon>;
69}
70
71impl IconNameExt for gpui_kit_assets::IconName {
72 fn view(self, cx: &mut App) -> Entity<Icon> {
73 Icon::build(self).view(cx)
74 }
75}
76
77#[derive(Clone)]
78pub(crate) enum IconSource {
79 Path(SharedString),
80 Data(Arc<[u8]>),
81}
82
83#[derive(Clone, IntoElement)]
84pub struct Icon {
85 style: StyleRefinement,
86 source: IconSource,
87 text_color: Option<Hsla>,
88 size: Option<Size>,
89 transformation: Option<Transformation>,
90}
91
92impl Default for Icon {
93 fn default() -> Self {
94 Self {
95 style: StyleRefinement::default(),
96 source: IconSource::Path("".into()),
97 text_color: None,
98 size: None,
99 transformation: None,
100 }
101 }
102}
103
104impl Icon {
105 pub fn new(icon: impl Into<Icon>) -> Self {
106 icon.into()
107 }
108
109 fn build(name: impl IconNamed) -> Self {
110 Self::default().path(name.path())
111 }
112
113 pub fn path(mut self, path: impl Into<SharedString>) -> Self {
118 self.source = IconSource::Path(path.into());
119 self
120 }
121
122 pub fn data(mut self, data: &[u8]) -> Self {
137 self.source = IconSource::Data(Arc::from(data));
138 self
139 }
140
141 #[cfg(any(target_os = "macos", target_os = "windows", test))]
142 pub(crate) fn source_ref(&self) -> &IconSource {
143 &self.source
144 }
145
146 pub(crate) fn same_layout(&self, other: &Self) -> bool {
149 self.style == other.style && self.size == other.size
150 }
151
152 pub fn view(self, cx: &mut App) -> Entity<Icon> {
154 cx.new(|_| self)
155 }
156
157 pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
159 self.transformation = Some(transformation);
160 self
161 }
162
163 pub fn empty() -> Self {
164 Self::default()
165 }
166
167 pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
171 self.transformation = Some(Transformation::rotate(radians));
172 self
173 }
174
175 fn into_svg(self, text_size: Pixels, fallback_color: Hsla) -> Svg {
176 let text_color = self.text_color.unwrap_or(fallback_color);
177 let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
178
179 svg()
180 .map(|mut this| {
181 *this.style() = self.style;
182 this
183 })
184 .flex_shrink_0()
185 .text_color(text_color)
186 .when(!has_base_size, |this| this.size(text_size))
187 .when_some(self.size, |this, size| match size {
188 Size::Size(px) => this.size(px),
189 Size::XSmall => this.size_3(),
190 Size::Small => this.size_3p5(),
191 Size::Medium => this.size_4(),
192 Size::Large => this.size_6(),
193 })
194 .map(|this| match self.source {
195 IconSource::Path(path) => this.path(path),
196 IconSource::Data(data) => this.data(&data),
197 })
198 .when_some(self.transformation, |this, transformation| {
199 this.with_transformation(transformation)
200 })
201 }
202}
203
204impl Styled for Icon {
205 fn style(&mut self) -> &mut StyleRefinement {
206 &mut self.style
207 }
208
209 fn text_color(mut self, color: impl Into<Hsla>) -> Self {
210 self.text_color = Some(color.into());
211 self
212 }
213}
214
215impl Sizable for Icon {
216 fn with_size(mut self, size: impl Into<Size>) -> Self {
217 self.size = Some(size.into());
218 self
219 }
220}
221
222impl RenderOnce for Icon {
223 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
224 let text_size = window.text_style().font_size.to_pixels(window.rem_size());
225 self.into_svg(text_size, window.text_style().color)
226 }
227}
228
229impl From<Icon> for AnyElement {
230 fn from(val: Icon) -> Self {
231 val.into_any_element()
232 }
233}
234
235impl Render for Icon {
236 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
237 let text_size = window.text_style().font_size.to_pixels(window.rem_size());
238 self.clone().into_svg(text_size, cx.theme().foreground)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use gpui::{px, size};
246
247 const SVG: &[u8] = include_bytes!("../../assets/assets/icons/arrow-up.svg");
248
249 #[test]
250 fn test_icon_builder_preserves_owned_data_and_transform_on_clone() {
251 let transformation = Transformation::scale(size(0.5, 0.5))
252 .with_rotation(gpui::radians(std::f32::consts::FRAC_PI_2));
253 let icon = {
254 let bytes = SVG.to_vec();
255 Icon::default()
256 .data(&bytes)
257 .large()
258 .text_color(gpui::red())
259 .transform(transformation)
260 };
261 let cloned = icon.clone();
262 let (IconSource::Data(original), IconSource::Data(copy)) = (&icon.source, &cloned.source)
263 else {
264 panic!("cloning must preserve the data source");
265 };
266 assert_eq!(copy.as_ref(), SVG);
267 assert!(Arc::ptr_eq(original, copy));
268 assert_eq!(cloned.transformation, Some(transformation));
269 assert_eq!(cloned.size, icon.size);
270 assert_eq!(cloned.text_color, icon.text_color);
271
272 let mut svg = cloned.into_svg(px(12.), gpui::blue());
273 assert_eq!(svg.style().text.color, Some(gpui::red()));
274 assert_eq!(svg.style().size.width, Some(gpui::rems(1.5).into()));
275
276 let rotated = icon.rotate(gpui::radians(std::f32::consts::PI)).clone();
277 assert_eq!(
278 rotated.transformation,
279 Some(Transformation::rotate(gpui::radians(std::f32::consts::PI)))
280 );
281 }
282
283 #[test]
284 fn test_icon_source_builders_replace_previous_source() {
285 let icon = Icon::new(IconName::Search).data(SVG);
286 assert!(matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == SVG));
287
288 let icon = icon.path("icons/replacement.svg");
289 assert!(
290 matches!(icon.source_ref(), IconSource::Path(path) if path == "icons/replacement.svg")
291 );
292
293 let icon = icon.data(SVG).data(b"replacement");
294 assert!(
295 matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == b"replacement")
296 );
297
298 let icon = icon.path("");
299 assert!(matches!(icon.source_ref(), IconSource::Path(path) if path.is_empty()));
300 }
301}