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 fn view(self, cx: &mut App) -> Entity<Icon> {
148 cx.new(|_| self)
149 }
150
151 pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
153 self.transformation = Some(transformation);
154 self
155 }
156
157 pub fn empty() -> Self {
158 Self::default()
159 }
160
161 pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
165 self.transformation = Some(Transformation::rotate(radians));
166 self
167 }
168
169 fn into_svg(self, text_size: Pixels, fallback_color: Hsla) -> Svg {
170 let text_color = self.text_color.unwrap_or(fallback_color);
171 let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
172
173 svg()
174 .map(|mut this| {
175 *this.style() = self.style;
176 this
177 })
178 .flex_shrink_0()
179 .text_color(text_color)
180 .when(!has_base_size, |this| this.size(text_size))
181 .when_some(self.size, |this, size| match size {
182 Size::Size(px) => this.size(px),
183 Size::XSmall => this.size_3(),
184 Size::Small => this.size_3p5(),
185 Size::Medium => this.size_4(),
186 Size::Large => this.size_6(),
187 })
188 .map(|this| match self.source {
189 IconSource::Path(path) => this.path(path),
190 IconSource::Data(data) => this.data(&data),
191 })
192 .when_some(self.transformation, |this, transformation| {
193 this.with_transformation(transformation)
194 })
195 }
196}
197
198impl Styled for Icon {
199 fn style(&mut self) -> &mut StyleRefinement {
200 &mut self.style
201 }
202
203 fn text_color(mut self, color: impl Into<Hsla>) -> Self {
204 self.text_color = Some(color.into());
205 self
206 }
207}
208
209impl Sizable for Icon {
210 fn with_size(mut self, size: impl Into<Size>) -> Self {
211 self.size = Some(size.into());
212 self
213 }
214}
215
216impl RenderOnce for Icon {
217 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
218 let text_size = window.text_style().font_size.to_pixels(window.rem_size());
219 self.into_svg(text_size, window.text_style().color)
220 }
221}
222
223impl From<Icon> for AnyElement {
224 fn from(val: Icon) -> Self {
225 val.into_any_element()
226 }
227}
228
229impl Render for Icon {
230 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
231 let text_size = window.text_style().font_size.to_pixels(window.rem_size());
232 self.clone().into_svg(text_size, cx.theme().foreground)
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use gpui::{px, size};
240
241 const SVG: &[u8] = include_bytes!("../../assets/assets/icons/arrow-up.svg");
242
243 #[test]
244 fn test_icon_builder_preserves_owned_data_and_transform_on_clone() {
245 let transformation = Transformation::scale(size(0.5, 0.5))
246 .with_rotation(gpui::radians(std::f32::consts::FRAC_PI_2));
247 let icon = {
248 let bytes = SVG.to_vec();
249 Icon::default()
250 .data(&bytes)
251 .large()
252 .text_color(gpui::red())
253 .transform(transformation)
254 };
255 let cloned = icon.clone();
256 let (IconSource::Data(original), IconSource::Data(copy)) = (&icon.source, &cloned.source)
257 else {
258 panic!("cloning must preserve the data source");
259 };
260 assert_eq!(copy.as_ref(), SVG);
261 assert!(Arc::ptr_eq(original, copy));
262 assert_eq!(cloned.transformation, Some(transformation));
263 assert_eq!(cloned.size, icon.size);
264 assert_eq!(cloned.text_color, icon.text_color);
265
266 let mut svg = cloned.into_svg(px(12.), gpui::blue());
267 assert_eq!(svg.style().text.color, Some(gpui::red()));
268 assert_eq!(svg.style().size.width, Some(gpui::rems(1.5).into()));
269
270 let rotated = icon.rotate(gpui::radians(std::f32::consts::PI)).clone();
271 assert_eq!(
272 rotated.transformation,
273 Some(Transformation::rotate(gpui::radians(std::f32::consts::PI)))
274 );
275 }
276
277 #[test]
278 fn test_icon_source_builders_replace_previous_source() {
279 let icon = Icon::new(IconName::Search).data(SVG);
280 assert!(matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == SVG));
281
282 let icon = icon.path("icons/replacement.svg");
283 assert!(
284 matches!(icon.source_ref(), IconSource::Path(path) if path == "icons/replacement.svg")
285 );
286
287 let icon = icon.data(SVG).data(b"replacement");
288 assert!(
289 matches!(icon.source_ref(), IconSource::Data(bytes) if bytes.as_ref() == b"replacement")
290 );
291
292 let icon = icon.path("");
293 assert!(matches!(icon.source_ref(), IconSource::Path(path) if path.is_empty()));
294 }
295}