1use gpui::{
18 Animation, AnimationExt, Hsla, Image, ImageFormat, ImageSource, Svg, Transformation, percentage,
19};
20use gpui_component::IconName;
21use native_theme::{AnimatedIcon, IconData, IconProvider, IconRole, load_custom_icon};
22use std::sync::Arc;
23use std::time::Duration;
24
25#[derive(Clone)]
30pub struct AnimatedImageSources {
31 pub sources: Vec<ImageSource>,
33 pub frame_duration_ms: u32,
35}
36
37impl std::fmt::Debug for AnimatedImageSources {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("AnimatedImageSources")
40 .field("frame_count", &self.sources.len())
41 .field("frame_duration_ms", &self.frame_duration_ms)
42 .finish()
43 }
44}
45
46#[must_use]
72pub fn icon_name(role: IconRole) -> Option<IconName> {
73 Some(match role {
74 IconRole::DialogWarning => IconName::TriangleAlert,
76 IconRole::DialogError => IconName::CircleX,
77 IconRole::DialogInfo => IconName::Info,
78 IconRole::DialogSuccess => IconName::CircleCheck,
79
80 IconRole::WindowClose => IconName::WindowClose,
82 IconRole::WindowMinimize => IconName::WindowMinimize,
83 IconRole::WindowMaximize => IconName::WindowMaximize,
84 IconRole::WindowRestore => IconName::WindowRestore,
85
86 IconRole::ActionDelete => IconName::Delete,
88 IconRole::ActionCopy => IconName::Copy,
89 IconRole::ActionUndo => IconName::Undo2,
90 IconRole::ActionRedo => IconName::Redo2,
91 IconRole::ActionSearch => IconName::Search,
92 IconRole::ActionSettings => IconName::Settings,
93 IconRole::ActionAdd => IconName::Plus,
94 IconRole::ActionRemove => IconName::Minus,
95
96 IconRole::NavBack => IconName::ChevronLeft,
98 IconRole::NavForward => IconName::ChevronRight,
99 IconRole::NavUp => IconName::ChevronUp,
100 IconRole::NavDown => IconName::ChevronDown,
101 IconRole::NavMenu => IconName::Menu,
102
103 IconRole::FileGeneric => IconName::File,
105 IconRole::FolderClosed => IconName::FolderClosed,
106 IconRole::FolderOpen => IconName::FolderOpen,
107 IconRole::TrashEmpty => IconName::Delete,
108
109 IconRole::StatusBusy => IconName::Loader,
111 IconRole::StatusCheck => IconName::Check,
112 IconRole::StatusError => IconName::CircleX,
113
114 IconRole::UserAccount => IconName::User,
116 IconRole::Notification => IconName::Bell,
117
118 _ => return None,
120 })
121}
122
123#[must_use]
130pub fn lucide_name_for_gpui_icon(icon: IconName) -> &'static str {
131 match icon {
132 IconName::ALargeSmall => "a-large-small",
133 IconName::ArrowDown => "arrow-down",
134 IconName::ArrowLeft => "arrow-left",
135 IconName::ArrowRight => "arrow-right",
136 IconName::ArrowUp => "arrow-up",
137 IconName::Asterisk => "asterisk",
138 IconName::Bell => "bell",
139 IconName::BookOpen => "book-open",
140 IconName::Bot => "bot",
141 IconName::Building2 => "building-2",
142 IconName::Calendar => "calendar",
143 IconName::CaseSensitive => "case-sensitive",
144 IconName::ChartPie => "chart-pie",
145 IconName::Check => "check",
146 IconName::ChevronDown => "chevron-down",
147 IconName::ChevronLeft => "chevron-left",
148 IconName::ChevronRight => "chevron-right",
149 IconName::ChevronsUpDown => "chevrons-up-down",
150 IconName::ChevronUp => "chevron-up",
151 IconName::CircleCheck => "circle-check",
152 IconName::CircleUser => "circle-user",
153 IconName::CircleX => "circle-x",
154 IconName::Close => "close",
155 IconName::Copy => "copy",
156 IconName::Dash => "dash",
157 IconName::Delete => "delete",
158 IconName::Ellipsis => "ellipsis",
159 IconName::EllipsisVertical => "ellipsis-vertical",
160 IconName::ExternalLink => "external-link",
161 IconName::Eye => "eye",
162 IconName::EyeOff => "eye-off",
163 IconName::File => "file",
164 IconName::Folder => "folder",
165 IconName::FolderClosed => "folder-closed",
166 IconName::FolderOpen => "folder-open",
167 IconName::Frame => "frame",
168 IconName::GalleryVerticalEnd => "gallery-vertical-end",
169 IconName::GitHub => "github",
170 IconName::Globe => "globe",
171 IconName::Heart => "heart",
172 IconName::HeartOff => "heart-off",
173 IconName::Inbox => "inbox",
174 IconName::Info => "info",
175 IconName::Inspector => "inspect",
176 IconName::LayoutDashboard => "layout-dashboard",
177 IconName::Loader => "loader",
178 IconName::LoaderCircle => "loader-circle",
179 IconName::Map => "map",
180 IconName::Maximize => "maximize",
181 IconName::Menu => "menu",
182 IconName::Minimize => "minimize",
183 IconName::Minus => "minus",
184 IconName::Moon => "moon",
185 IconName::Palette => "palette",
186 IconName::PanelBottom => "panel-bottom",
187 IconName::PanelBottomOpen => "panel-bottom-open",
188 IconName::PanelLeft => "panel-left",
189 IconName::PanelLeftClose => "panel-left-close",
190 IconName::PanelLeftOpen => "panel-left-open",
191 IconName::PanelRight => "panel-right",
192 IconName::PanelRightClose => "panel-right-close",
193 IconName::PanelRightOpen => "panel-right-open",
194 IconName::Plus => "plus",
195 IconName::Redo => "redo",
196 IconName::Redo2 => "redo-2",
197 IconName::Replace => "replace",
198 IconName::ResizeCorner => "resize-corner",
199 IconName::Search => "search",
200 IconName::Settings => "settings",
201 IconName::Settings2 => "settings-2",
202 IconName::SortAscending => "sort-ascending",
203 IconName::SortDescending => "sort-descending",
204 IconName::SquareTerminal => "square-terminal",
205 IconName::Star => "star",
206 IconName::StarOff => "star-off",
207 IconName::Sun => "sun",
208 IconName::ThumbsDown => "thumbs-down",
209 IconName::ThumbsUp => "thumbs-up",
210 IconName::TriangleAlert => "triangle-alert",
211 IconName::Undo => "undo",
212 IconName::Undo2 => "undo-2",
213 IconName::User => "user",
214 IconName::WindowClose => "window-close",
215 IconName::WindowMaximize => "window-maximize",
216 IconName::WindowMinimize => "window-minimize",
217 IconName::WindowRestore => "window-restore",
218 }
219}
220
221#[must_use]
228pub fn material_name_for_gpui_icon(icon: IconName) -> &'static str {
229 match icon {
230 IconName::ALargeSmall => "font_size",
231 IconName::ArrowDown => "arrow_downward",
232 IconName::ArrowLeft => "arrow_back",
233 IconName::ArrowRight => "arrow_forward",
234 IconName::ArrowUp => "arrow_upward",
235 IconName::Asterisk => "emergency",
236 IconName::Bell => "notifications",
237 IconName::BookOpen => "menu_book",
238 IconName::Bot => "smart_toy",
239 IconName::Building2 => "apartment",
240 IconName::Calendar => "calendar_today",
241 IconName::CaseSensitive => "match_case",
242 IconName::ChartPie => "pie_chart",
243 IconName::Check => "check",
244 IconName::ChevronDown => "expand_more",
245 IconName::ChevronLeft => "chevron_left",
246 IconName::ChevronRight => "chevron_right",
247 IconName::ChevronsUpDown => "unfold_more",
248 IconName::ChevronUp => "expand_less",
249 IconName::CircleCheck => "check_circle",
250 IconName::CircleUser => "account_circle",
251 IconName::CircleX => "cancel",
252 IconName::Close => "close",
253 IconName::Copy => "content_copy",
254 IconName::Dash => "remove",
255 IconName::Delete => "delete",
256 IconName::Ellipsis => "more_horiz",
257 IconName::EllipsisVertical => "more_vert",
258 IconName::ExternalLink => "open_in_new",
259 IconName::Eye => "visibility",
260 IconName::EyeOff => "visibility_off",
261 IconName::File => "description",
262 IconName::Folder => "folder",
263 IconName::FolderClosed => "folder",
264 IconName::FolderOpen => "folder_open",
265 IconName::Frame => "crop_free",
266 IconName::GalleryVerticalEnd => "view_carousel",
267 IconName::GitHub => "code",
268 IconName::Globe => "language",
269 IconName::Heart => "favorite",
270 IconName::HeartOff => "heart_broken",
271 IconName::Inbox => "inbox",
272 IconName::Info => "info",
273 IconName::Inspector => "developer_mode",
274 IconName::LayoutDashboard => "dashboard",
275 IconName::Loader => "progress_activity",
276 IconName::LoaderCircle => "autorenew",
277 IconName::Map => "map",
278 IconName::Maximize => "open_in_full",
279 IconName::Menu => "menu",
280 IconName::Minimize => "minimize",
281 IconName::Minus => "remove",
282 IconName::Moon => "dark_mode",
283 IconName::Palette => "palette",
284 IconName::PanelBottom => "dock_to_bottom",
285 IconName::PanelBottomOpen => "web_asset",
286 IconName::PanelLeft => "side_navigation",
287 IconName::PanelLeftClose => "left_panel_close",
288 IconName::PanelLeftOpen => "left_panel_open",
289 IconName::PanelRight => "right_panel_close",
290 IconName::PanelRightClose => "right_panel_close",
291 IconName::PanelRightOpen => "right_panel_open",
292 IconName::Plus => "add",
293 IconName::Redo => "redo",
294 IconName::Redo2 => "redo",
295 IconName::Replace => "find_replace",
296 IconName::ResizeCorner => "drag_indicator",
297 IconName::Search => "search",
298 IconName::Settings => "settings",
299 IconName::Settings2 => "tune",
300 IconName::SortAscending => "arrow_upward",
301 IconName::SortDescending => "arrow_downward",
302 IconName::SquareTerminal => "terminal",
303 IconName::Star => "star",
304 IconName::StarOff => "star_border",
305 IconName::Sun => "light_mode",
306 IconName::ThumbsDown => "thumb_down",
307 IconName::ThumbsUp => "thumb_up",
308 IconName::TriangleAlert => "warning",
309 IconName::Undo => "undo",
310 IconName::Undo2 => "undo",
311 IconName::User => "person",
312 IconName::WindowClose => "close",
313 IconName::WindowMaximize => "open_in_full",
314 IconName::WindowMinimize => "minimize",
315 IconName::WindowRestore => "close_fullscreen",
316 }
317}
318
319#[cfg(target_os = "linux")]
340#[must_use]
341pub fn freedesktop_name_for_gpui_icon(
342 icon: IconName,
343 de: native_theme::LinuxDesktop,
344) -> &'static str {
345 use native_theme::LinuxDesktop;
346
347 let is_gtk = matches!(
349 de,
350 LinuxDesktop::Gnome
351 | LinuxDesktop::Budgie
352 | LinuxDesktop::Cinnamon
353 | LinuxDesktop::Mate
354 | LinuxDesktop::Xfce
355 );
356
357 match icon {
358 IconName::BookOpen => "help-contents", IconName::Bot => "face-smile", IconName::ChevronDown => "go-down", IconName::ChevronLeft => "go-previous", IconName::ChevronRight => "go-next", IconName::ChevronUp => "go-up", IconName::CircleX => "dialog-error", IconName::Copy => "edit-copy", IconName::Dash => "list-remove", IconName::Delete => "edit-delete", IconName::File => "text-x-generic", IconName::Folder => "folder", IconName::FolderClosed => "folder", IconName::FolderOpen => "folder-open", IconName::HeartOff => "non-starred", IconName::Info => "dialog-information", IconName::LayoutDashboard => "view-grid", IconName::Map => "find-location", IconName::Maximize => "view-fullscreen", IconName::Menu => "open-menu", IconName::Minimize => "window-minimize", IconName::Minus => "list-remove", IconName::Moon => "weather-clear-night", IconName::Plus => "list-add", IconName::Redo => "edit-redo", IconName::Redo2 => "edit-redo", IconName::Replace => "edit-find-replace", IconName::Search => "edit-find", IconName::Settings => "preferences-system", IconName::SortAscending => "view-sort-ascending", IconName::SortDescending => "view-sort-descending", IconName::SquareTerminal => "utilities-terminal", IconName::Star => "starred", IconName::StarOff => "non-starred", IconName::Sun => "weather-clear", IconName::TriangleAlert => "dialog-warning", IconName::Undo => "edit-undo", IconName::Undo2 => "edit-undo", IconName::User => "system-users", IconName::WindowClose => "window-close", IconName::WindowMaximize => "window-maximize", IconName::WindowMinimize => "window-minimize", IconName::WindowRestore => "window-restore", IconName::ArrowDown => {
405 if is_gtk {
406 "go-bottom"
407 } else {
408 "go-down-skip"
409 }
410 } IconName::ArrowLeft => {
412 if is_gtk {
413 "go-first"
414 } else {
415 "go-previous-skip"
416 }
417 } IconName::ArrowRight => {
419 if is_gtk {
420 "go-last"
421 } else {
422 "go-next-skip"
423 }
424 } IconName::ArrowUp => {
426 if is_gtk {
427 "go-top"
428 } else {
429 "go-up-skip"
430 }
431 } IconName::Calendar => {
433 if is_gtk {
434 "x-office-calendar"
435 } else {
436 "view-calendar"
437 }
438 } IconName::Check => {
440 if is_gtk {
441 "object-select"
442 } else {
443 "dialog-ok"
444 }
445 } IconName::CircleCheck => {
447 if is_gtk {
448 "object-select"
449 } else {
450 "emblem-ok-symbolic"
451 }
452 } IconName::CircleUser => {
454 if is_gtk {
455 "avatar-default"
456 } else {
457 "user-identity"
458 }
459 } IconName::Close => {
461 if is_gtk {
462 "window-close"
463 } else {
464 "tab-close"
465 }
466 } IconName::Ellipsis => {
468 if is_gtk {
469 "view-more-horizontal"
470 } else {
471 "overflow-menu"
472 }
473 } IconName::EllipsisVertical => {
475 if is_gtk {
476 "view-more"
477 } else {
478 "overflow-menu"
479 }
480 } IconName::Eye => {
482 if is_gtk {
483 "view-reveal"
484 } else {
485 "view-visible"
486 }
487 } IconName::EyeOff => {
489 if is_gtk {
490 "view-conceal"
491 } else {
492 "view-hidden"
493 }
494 } IconName::Frame => {
496 if is_gtk {
497 "selection-mode"
498 } else {
499 "select-rectangular"
500 }
501 } IconName::Heart => {
503 if is_gtk {
504 "starred"
505 } else {
506 "emblem-favorite"
507 }
508 } IconName::Loader => {
510 if is_gtk {
511 "content-loading"
512 } else {
513 "process-working"
514 }
515 } IconName::LoaderCircle => {
517 if is_gtk {
518 "content-loading"
519 } else {
520 "process-working"
521 }
522 } IconName::Palette => {
524 if is_gtk {
525 "color-select"
526 } else {
527 "palette"
528 }
529 } IconName::PanelLeft => {
531 if is_gtk {
532 "sidebar-show"
533 } else {
534 "sidebar-expand-left"
535 }
536 } IconName::PanelLeftClose => {
538 if is_gtk {
539 "sidebar-show"
540 } else {
541 "view-left-close"
542 }
543 } IconName::PanelLeftOpen => {
545 if is_gtk {
546 "sidebar-show"
547 } else {
548 "view-left-new"
549 }
550 } IconName::PanelRight => {
552 if is_gtk {
553 "sidebar-show-right"
554 } else {
555 "view-right-new"
556 }
557 } IconName::PanelRightClose => {
559 if is_gtk {
560 "sidebar-show-right"
561 } else {
562 "view-right-close"
563 }
564 } IconName::PanelRightOpen => {
566 if is_gtk {
567 "sidebar-show-right"
568 } else {
569 "view-right-new"
570 }
571 } IconName::ResizeCorner => {
573 if is_gtk {
574 "list-drag-handle"
575 } else {
576 "drag-handle"
577 }
578 } IconName::Settings2 => {
580 if is_gtk {
581 "preferences-other"
582 } else {
583 "configure"
584 }
585 } IconName::ALargeSmall => {
589 if is_gtk {
590 "zoom-in"
591 } else {
592 "format-font-size-more"
593 }
594 } IconName::Asterisk => {
596 if is_gtk {
597 "starred"
598 } else {
599 "rating"
600 }
601 } IconName::Bell => {
603 if is_gtk {
604 "alarm"
605 } else {
606 "notification-active"
607 }
608 } IconName::Building2 => {
610 if is_gtk {
611 "network-workgroup"
612 } else {
613 "applications-office"
614 }
615 } IconName::CaseSensitive => {
617 if is_gtk {
618 "format-text-rich"
619 } else {
620 "format-text-uppercase"
621 }
622 } IconName::ChartPie => {
624 if is_gtk {
625 "x-office-spreadsheet"
626 } else {
627 "office-chart-pie"
628 }
629 } IconName::ChevronsUpDown => {
631 if is_gtk {
632 "list-drag-handle"
633 } else {
634 "handle-sort"
635 }
636 } IconName::ExternalLink => {
638 if is_gtk {
639 "insert-link"
640 } else {
641 "external-link"
642 }
643 } IconName::GalleryVerticalEnd => {
645 if is_gtk {
646 "view-paged"
647 } else {
648 "view-list-icons"
649 }
650 } IconName::GitHub => {
652 if is_gtk {
653 "applications-engineering"
654 } else {
655 "vcs-branch"
656 }
657 } IconName::Globe => {
659 if is_gtk {
660 "web-browser"
661 } else {
662 "globe"
663 }
664 } IconName::Inbox => {
666 if is_gtk {
667 "mail-send-receive"
668 } else {
669 "mail-folder-inbox"
670 }
671 } IconName::Inspector => {
673 if is_gtk {
674 "preferences-system-details"
675 } else {
676 "code-context"
677 }
678 } IconName::PanelBottom => {
680 if is_gtk {
681 "view-dual"
682 } else {
683 "view-split-top-bottom"
684 }
685 } IconName::PanelBottomOpen => {
687 if is_gtk {
688 "view-dual"
689 } else {
690 "view-split-top-bottom"
691 }
692 } IconName::ThumbsDown => {
694 if is_gtk {
695 "process-stop"
696 } else {
697 "rating-unrated"
698 }
699 } IconName::ThumbsUp => {
701 if is_gtk {
702 "checkbox-checked"
703 } else {
704 "approved"
705 }
706 } }
708}
709
710const SVG_RASTERIZE_SIZE: u32 = 48;
715
716#[must_use]
744pub fn to_image_source(
745 data: &IconData,
746 color: Option<Hsla>,
747 size: Option<u32>,
748) -> Option<ImageSource> {
749 let raster_size = size.unwrap_or(SVG_RASTERIZE_SIZE);
750 match data {
751 IconData::Svg(bytes) => {
752 if let Some(c) = color {
753 let colored = colorize_svg(bytes, c);
754 svg_to_bmp_source(&colored, raster_size)
755 } else {
756 svg_to_bmp_source(bytes, raster_size)
757 }
758 }
759 IconData::Rgba {
760 width,
761 height,
762 data,
763 } => {
764 let bmp = encode_rgba_as_bmp(*width, *height, data);
765 let image = Image::from_bytes(ImageFormat::Bmp, bmp);
766 Some(ImageSource::Image(Arc::new(image)))
767 }
768 _ => None,
769 }
770}
771
772#[must_use]
783pub fn into_image_source(
784 data: IconData,
785 color: Option<Hsla>,
786 size: Option<u32>,
787) -> Option<ImageSource> {
788 let raster_size = size.unwrap_or(SVG_RASTERIZE_SIZE);
789 match data {
790 IconData::Svg(bytes) => {
791 if let Some(c) = color {
792 let colored = colorize_svg(&bytes, c);
793 svg_to_bmp_source(&colored, raster_size)
794 } else {
795 svg_to_bmp_source(&bytes, raster_size)
796 }
797 }
798 IconData::Rgba {
799 width,
800 height,
801 data,
802 } => {
803 let bmp = encode_rgba_as_bmp(width, height, &data);
804 let image = Image::from_bytes(ImageFormat::Bmp, bmp);
805 Some(ImageSource::Image(Arc::new(image)))
806 }
807 _ => None,
808 }
809}
810
811#[must_use]
821pub fn custom_icon_to_image_source(
822 provider: &(impl IconProvider + ?Sized),
823 icon_set: native_theme::IconSet,
824 color: Option<Hsla>,
825 size: Option<u32>,
826) -> Option<ImageSource> {
827 let data = load_custom_icon(provider, icon_set)?;
828 to_image_source(&data, color, size)
829}
830
831#[must_use]
859pub fn animated_frames_to_image_sources(anim: &AnimatedIcon) -> Option<AnimatedImageSources> {
860 match anim {
861 AnimatedIcon::Frames {
862 frames,
863 frame_duration_ms,
864 } => {
865 let sources: Vec<ImageSource> = frames
866 .iter()
867 .filter_map(|f| to_image_source(f, None, None))
868 .collect();
869 if sources.is_empty() {
870 None
871 } else {
872 Some(AnimatedImageSources {
873 sources,
874 frame_duration_ms: *frame_duration_ms,
875 })
876 }
877 }
878 _ => None,
879 }
880}
881
882#[must_use]
908pub fn with_spin_animation(
909 element: Svg,
910 animation_id: impl Into<gpui::ElementId>,
911 duration_ms: u32,
912) -> impl gpui::IntoElement {
913 element.with_animation(
914 animation_id,
915 Animation::new(Duration::from_millis(duration_ms as u64)).repeat(),
916 |el, delta| el.with_transformation(Transformation::rotate(percentage(delta))),
917 )
918}
919
920fn svg_to_bmp_source(svg_bytes: &[u8], size: u32) -> Option<ImageSource> {
928 let Ok(IconData::Rgba {
929 width,
930 height,
931 data,
932 }) = native_theme::rasterize::rasterize_svg(svg_bytes, size)
933 else {
934 return None;
935 };
936 let bmp = encode_rgba_as_bmp(width, height, &data);
937 let image = Image::from_bytes(ImageFormat::Bmp, bmp);
938 Some(ImageSource::Image(Arc::new(image)))
939}
940
941fn colorize_svg(svg_bytes: &[u8], color: Hsla) -> Vec<u8> {
955 let rgba: gpui::Rgba = color.into();
956 let r = (rgba.r.clamp(0.0, 1.0) * 255.0).round() as u8;
957 let g = (rgba.g.clamp(0.0, 1.0) * 255.0).round() as u8;
958 let b = (rgba.b.clamp(0.0, 1.0) * 255.0).round() as u8;
959 let hex = format!("#{:02x}{:02x}{:02x}", r, g, b);
960
961 let svg_str = String::from_utf8_lossy(svg_bytes);
962
963 if svg_str.contains("currentColor") {
965 return svg_str.replace("currentColor", &hex).into_bytes();
966 }
967
968 let fill_hex = format!("fill=\"{}\"", hex);
970 let replaced = svg_str
971 .replace("fill=\"black\"", &fill_hex)
972 .replace("fill=\"#000000\"", &fill_hex)
973 .replace("fill=\"#000\"", &fill_hex);
974 if replaced != svg_str {
975 return replaced.into_bytes();
976 }
977
978 if let Some(pos) = svg_str.find("<svg")
981 && let Some(close) = svg_str[pos..].find('>')
982 {
983 let tag_end = pos + close;
984 let tag = &svg_str[pos..tag_end];
985 if !tag.contains("fill=") {
986 let inject_pos = if tag_end > 0 && svg_str.as_bytes()[tag_end - 1] == b'/' {
988 tag_end - 1
989 } else {
990 tag_end
991 };
992 let mut result = String::with_capacity(svg_str.len() + 20);
993 result.push_str(&svg_str[..inject_pos]);
994 result.push_str(&format!(" fill=\"{}\"", hex));
995 result.push_str(&svg_str[inject_pos..]);
996 return result.into_bytes();
997 }
998 }
999
1000 svg_bytes.to_vec()
1002}
1003
1004fn encode_rgba_as_bmp(width: u32, height: u32, rgba: &[u8]) -> Vec<u8> {
1009 let pixel_data_size = (width * height * 4) as usize;
1010 let header_size: u32 = 14; let dib_header_size: u32 = 108; let file_size = header_size + dib_header_size + pixel_data_size as u32;
1013
1014 let mut buf = Vec::with_capacity(file_size as usize);
1015
1016 buf.extend_from_slice(b"BM"); buf.extend_from_slice(&file_size.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&(header_size + dib_header_size).to_le_bytes()); buf.extend_from_slice(&dib_header_size.to_le_bytes()); buf.extend_from_slice(&(width as i32).to_le_bytes()); buf.extend_from_slice(&(-(height as i32)).to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&32u16.to_le_bytes()); buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(&(pixel_data_size as u32).to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&2835u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0x00FF0000u32.to_le_bytes()); buf.extend_from_slice(&0x0000FF00u32.to_le_bytes()); buf.extend_from_slice(&0x000000FFu32.to_le_bytes()); buf.extend_from_slice(&0xFF000000u32.to_le_bytes()); buf.extend_from_slice(&0x73524742u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 36]);
1048
1049 buf.extend_from_slice(&0u32.to_le_bytes());
1051 buf.extend_from_slice(&0u32.to_le_bytes());
1052 buf.extend_from_slice(&0u32.to_le_bytes());
1053
1054 for pixel in rgba.chunks_exact(4) {
1056 buf.push(pixel[2]); buf.push(pixel[1]); buf.push(pixel[0]); buf.push(pixel[3]); }
1061
1062 buf
1063}
1064
1065#[cfg(test)]
1066#[allow(clippy::unwrap_used, clippy::expect_used)]
1067mod tests {
1068 use super::*;
1069
1070 pub(super) const ALL_ICON_NAMES: &[IconName] = &[
1071 IconName::ALargeSmall,
1072 IconName::ArrowDown,
1073 IconName::ArrowLeft,
1074 IconName::ArrowRight,
1075 IconName::ArrowUp,
1076 IconName::Asterisk,
1077 IconName::Bell,
1078 IconName::BookOpen,
1079 IconName::Bot,
1080 IconName::Building2,
1081 IconName::Calendar,
1082 IconName::CaseSensitive,
1083 IconName::ChartPie,
1084 IconName::Check,
1085 IconName::ChevronDown,
1086 IconName::ChevronLeft,
1087 IconName::ChevronRight,
1088 IconName::ChevronsUpDown,
1089 IconName::ChevronUp,
1090 IconName::CircleCheck,
1091 IconName::CircleUser,
1092 IconName::CircleX,
1093 IconName::Close,
1094 IconName::Copy,
1095 IconName::Dash,
1096 IconName::Delete,
1097 IconName::Ellipsis,
1098 IconName::EllipsisVertical,
1099 IconName::ExternalLink,
1100 IconName::Eye,
1101 IconName::EyeOff,
1102 IconName::File,
1103 IconName::Folder,
1104 IconName::FolderClosed,
1105 IconName::FolderOpen,
1106 IconName::Frame,
1107 IconName::GalleryVerticalEnd,
1108 IconName::GitHub,
1109 IconName::Globe,
1110 IconName::Heart,
1111 IconName::HeartOff,
1112 IconName::Inbox,
1113 IconName::Info,
1114 IconName::Inspector,
1115 IconName::LayoutDashboard,
1116 IconName::Loader,
1117 IconName::LoaderCircle,
1118 IconName::Map,
1119 IconName::Maximize,
1120 IconName::Menu,
1121 IconName::Minimize,
1122 IconName::Minus,
1123 IconName::Moon,
1124 IconName::Palette,
1125 IconName::PanelBottom,
1126 IconName::PanelBottomOpen,
1127 IconName::PanelLeft,
1128 IconName::PanelLeftClose,
1129 IconName::PanelLeftOpen,
1130 IconName::PanelRight,
1131 IconName::PanelRightClose,
1132 IconName::PanelRightOpen,
1133 IconName::Plus,
1134 IconName::Redo,
1135 IconName::Redo2,
1136 IconName::Replace,
1137 IconName::ResizeCorner,
1138 IconName::Search,
1139 IconName::Settings,
1140 IconName::Settings2,
1141 IconName::SortAscending,
1142 IconName::SortDescending,
1143 IconName::SquareTerminal,
1144 IconName::Star,
1145 IconName::StarOff,
1146 IconName::Sun,
1147 IconName::ThumbsDown,
1148 IconName::ThumbsUp,
1149 IconName::TriangleAlert,
1150 IconName::Undo,
1151 IconName::Undo2,
1152 IconName::User,
1153 IconName::WindowClose,
1154 IconName::WindowMaximize,
1155 IconName::WindowMinimize,
1156 IconName::WindowRestore,
1157 ];
1158
1159 #[test]
1160 fn all_icons_have_lucide_mapping() {
1161 for icon in ALL_ICON_NAMES {
1162 let name = lucide_name_for_gpui_icon(icon.clone());
1163 assert!(
1164 !name.is_empty(),
1165 "Empty Lucide mapping for an IconName variant",
1166 );
1167 }
1168 }
1169
1170 #[test]
1171 fn all_icons_have_material_mapping() {
1172 for icon in ALL_ICON_NAMES {
1173 let name = material_name_for_gpui_icon(icon.clone());
1174 assert!(
1175 !name.is_empty(),
1176 "Empty Material mapping for an IconName variant",
1177 );
1178 }
1179 }
1180
1181 #[test]
1184 fn icon_name_dialog_warning_maps_to_triangle_alert() {
1185 assert!(matches!(
1186 icon_name(IconRole::DialogWarning),
1187 Some(IconName::TriangleAlert)
1188 ));
1189 }
1190
1191 #[test]
1192 fn icon_name_dialog_error_maps_to_circle_x() {
1193 assert!(matches!(
1194 icon_name(IconRole::DialogError),
1195 Some(IconName::CircleX)
1196 ));
1197 }
1198
1199 #[test]
1200 fn icon_name_dialog_info_maps_to_info() {
1201 assert!(matches!(
1202 icon_name(IconRole::DialogInfo),
1203 Some(IconName::Info)
1204 ));
1205 }
1206
1207 #[test]
1208 fn icon_name_dialog_success_maps_to_circle_check() {
1209 assert!(matches!(
1210 icon_name(IconRole::DialogSuccess),
1211 Some(IconName::CircleCheck)
1212 ));
1213 }
1214
1215 #[test]
1216 fn icon_name_window_close_maps() {
1217 assert!(matches!(
1218 icon_name(IconRole::WindowClose),
1219 Some(IconName::WindowClose)
1220 ));
1221 }
1222
1223 #[test]
1224 fn icon_name_action_copy_maps_to_copy() {
1225 assert!(matches!(
1226 icon_name(IconRole::ActionCopy),
1227 Some(IconName::Copy)
1228 ));
1229 }
1230
1231 #[test]
1232 fn icon_name_nav_back_maps_to_chevron_left() {
1233 assert!(matches!(
1234 icon_name(IconRole::NavBack),
1235 Some(IconName::ChevronLeft)
1236 ));
1237 }
1238
1239 #[test]
1240 fn icon_name_file_generic_maps_to_file() {
1241 assert!(matches!(
1242 icon_name(IconRole::FileGeneric),
1243 Some(IconName::File)
1244 ));
1245 }
1246
1247 #[test]
1248 fn icon_name_status_check_maps_to_check() {
1249 assert!(matches!(
1250 icon_name(IconRole::StatusCheck),
1251 Some(IconName::Check)
1252 ));
1253 }
1254
1255 #[test]
1256 fn icon_name_user_account_maps_to_user() {
1257 assert!(matches!(
1258 icon_name(IconRole::UserAccount),
1259 Some(IconName::User)
1260 ));
1261 }
1262
1263 #[test]
1264 fn icon_name_notification_maps_to_bell() {
1265 assert!(matches!(
1266 icon_name(IconRole::Notification),
1267 Some(IconName::Bell)
1268 ));
1269 }
1270
1271 #[test]
1273 fn icon_name_shield_returns_none() {
1274 assert!(icon_name(IconRole::Shield).is_none());
1275 }
1276
1277 #[test]
1278 fn icon_name_lock_returns_none() {
1279 assert!(icon_name(IconRole::Lock).is_none());
1280 }
1281
1282 #[test]
1283 fn icon_name_action_save_returns_none() {
1284 assert!(icon_name(IconRole::ActionSave).is_none());
1285 }
1286
1287 #[test]
1288 fn icon_name_help_returns_none() {
1289 assert!(icon_name(IconRole::Help).is_none());
1290 }
1291
1292 #[test]
1293 fn icon_name_dialog_question_returns_none() {
1294 assert!(icon_name(IconRole::DialogQuestion).is_none());
1295 }
1296
1297 #[test]
1299 fn icon_name_maps_at_least_28_roles() {
1300 let some_count = IconRole::ALL
1301 .iter()
1302 .filter(|r| icon_name(**r).is_some())
1303 .count();
1304 assert!(
1305 some_count >= 28,
1306 "Expected at least 28 mappings, got {}",
1307 some_count
1308 );
1309 }
1310
1311 #[test]
1312 fn icon_name_maps_exactly_30_roles() {
1313 let some_count = IconRole::ALL
1314 .iter()
1315 .filter(|r| icon_name(**r).is_some())
1316 .count();
1317 assert_eq!(
1318 some_count, 30,
1319 "Expected exactly 30 mappings, got {some_count}"
1320 );
1321 }
1322
1323 #[test]
1326 fn to_image_source_svg_returns_bmp_rasterized() {
1327 let svg = IconData::Svg(
1329 b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='red'/></svg>".to_vec(),
1330 );
1331 let source = to_image_source(&svg, None, None).expect("valid SVG should convert");
1332 match source {
1334 ImageSource::Image(arc) => {
1335 assert_eq!(arc.format, ImageFormat::Bmp);
1336 assert!(arc.bytes.starts_with(b"BM"), "BMP should start with 'BM'");
1337 }
1338 _ => panic!("Expected ImageSource::Image for SVG data"),
1339 }
1340 }
1341
1342 #[test]
1343 fn to_image_source_rgba_returns_bmp_image_source() {
1344 let rgba = IconData::Rgba {
1345 width: 2,
1346 height: 2,
1347 data: vec![
1348 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255, ],
1353 };
1354 let source = to_image_source(&rgba, None, None).expect("RGBA should convert");
1355 match source {
1356 ImageSource::Image(arc) => {
1357 assert_eq!(arc.format, ImageFormat::Bmp);
1358 assert_eq!(&arc.bytes[0..2], b"BM");
1360 }
1361 _ => panic!("Expected ImageSource::Image for RGBA data"),
1362 }
1363 }
1364
1365 #[test]
1366 fn to_image_source_with_color() {
1367 let svg = IconData::Svg(
1368 b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M0 0' stroke='currentColor'/></svg>".to_vec(),
1369 );
1370 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0);
1371 let result = to_image_source(&svg, Some(color), None);
1372 assert!(result.is_some(), "colorized SVG should convert");
1373 }
1374
1375 #[test]
1376 fn to_image_source_with_custom_size() {
1377 let svg = IconData::Svg(
1378 b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='red'/></svg>".to_vec(),
1379 );
1380 let result = to_image_source(&svg, None, Some(32));
1381 assert!(result.is_some(), "custom size SVG should convert");
1382 }
1383
1384 #[test]
1387 fn encode_rgba_as_bmp_correct_file_size() {
1388 let rgba = vec![0u8; 4 * 4 * 4]; let bmp = encode_rgba_as_bmp(4, 4, &rgba);
1390 let expected_size = 14 + 108 + (4 * 4 * 4); assert_eq!(bmp.len(), expected_size);
1392 }
1393
1394 #[test]
1395 fn encode_rgba_as_bmp_starts_with_bm() {
1396 let rgba = vec![0u8; 4]; let bmp = encode_rgba_as_bmp(1, 1, &rgba);
1398 assert_eq!(&bmp[0..2], b"BM");
1399 }
1400
1401 #[test]
1402 fn encode_rgba_as_bmp_pixel_order_is_bgra() {
1403 let rgba = vec![0xAA, 0xBB, 0xCC, 0xDD];
1405 let bmp = encode_rgba_as_bmp(1, 1, &rgba);
1406 let pixel_offset = (14 + 108) as usize;
1407 assert_eq!(bmp[pixel_offset], 0xCC); assert_eq!(bmp[pixel_offset + 1], 0xBB); assert_eq!(bmp[pixel_offset + 2], 0xAA); assert_eq!(bmp[pixel_offset + 3], 0xDD); }
1413 #[test]
1416 fn colorize_svg_replaces_fill_black() {
1417 let svg = b"<svg><path fill=\"black\" d=\"M0 0h24v24H0z\"/></svg>";
1418 let color = gpui::hsla(0.6, 0.7, 0.5, 1.0); let result = colorize_svg(svg, color);
1420 let result_str = String::from_utf8(result).unwrap();
1421 assert!(
1422 !result_str.contains("fill=\"black\""),
1423 "fill=\"black\" should be replaced, got: {}",
1424 result_str
1425 );
1426 assert!(
1427 result_str.contains("fill=\"#"),
1428 "should contain hex fill, got: {}",
1429 result_str
1430 );
1431 }
1432
1433 #[test]
1434 fn colorize_svg_replaces_fill_hex_black() {
1435 let svg = b"<svg><rect fill=\"#000000\" width=\"24\" height=\"24\"/></svg>";
1436 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0); let result = colorize_svg(svg, color);
1438 let result_str = String::from_utf8(result).unwrap();
1439 assert!(
1440 !result_str.contains("#000000"),
1441 "fill=\"#000000\" should be replaced, got: {}",
1442 result_str
1443 );
1444 }
1445
1446 #[test]
1447 fn colorize_svg_replaces_fill_short_hex_black() {
1448 let svg = b"<svg><rect fill=\"#000\" width=\"24\" height=\"24\"/></svg>";
1449 let color = gpui::hsla(0.3, 0.8, 0.4, 1.0); let result = colorize_svg(svg, color);
1451 let result_str = String::from_utf8(result).unwrap();
1452 assert!(
1453 !result_str.contains("fill=\"#000\""),
1454 "fill=\"#000\" should be replaced, got: {}",
1455 result_str
1456 );
1457 }
1458
1459 #[test]
1460 fn colorize_svg_current_color_still_works() {
1461 let svg = b"<svg><path stroke=\"currentColor\" d=\"M0 0\"/></svg>";
1462 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0);
1463 let result = colorize_svg(svg, color);
1464 let result_str = String::from_utf8(result).unwrap();
1465 assert!(
1466 !result_str.contains("currentColor"),
1467 "currentColor should be replaced"
1468 );
1469 assert!(result_str.contains('#'), "should contain hex color");
1470 }
1471
1472 #[test]
1473 fn colorize_svg_implicit_black_still_works() {
1474 let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0\"/></svg>";
1476 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0);
1477 let result = colorize_svg(svg, color);
1478 let result_str = String::from_utf8(result).unwrap();
1479 assert!(
1480 result_str.contains("fill=\"#"),
1481 "should inject fill into root svg tag, got: {}",
1482 result_str
1483 );
1484 }
1485
1486 #[test]
1487 fn colorize_self_closing_svg_produces_valid_xml() {
1488 let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\" />";
1490 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0);
1491 let result = colorize_svg(svg, color);
1492 let result_str = String::from_utf8(result).unwrap();
1493 assert!(
1494 result_str.contains("fill=\"#"),
1495 "should inject fill, got: {}",
1496 result_str
1497 );
1498 assert!(
1500 !result_str.contains("/ fill="),
1501 "fill must be before '/', got: {}",
1502 result_str
1503 );
1504 assert!(
1506 result_str.trim().ends_with("/>"),
1507 "should remain self-closing, got: {}",
1508 result_str
1509 );
1510 }
1511
1512 #[test]
1515 fn into_image_source_svg_returns_some() {
1516 let svg = IconData::Svg(
1517 b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='red'/></svg>".to_vec(),
1518 );
1519 let result = into_image_source(svg, None, None);
1520 assert!(result.is_some(), "valid SVG should convert");
1521 }
1522
1523 #[test]
1524 fn into_image_source_rgba_returns_some() {
1525 let rgba = IconData::Rgba {
1526 width: 2,
1527 height: 2,
1528 data: vec![
1529 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
1530 ],
1531 };
1532 let result = into_image_source(rgba, None, None);
1533 assert!(result.is_some(), "RGBA should convert");
1534 }
1535
1536 #[test]
1537 fn into_image_source_with_color() {
1538 let svg = IconData::Svg(
1539 b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M0 0' stroke='currentColor'/></svg>".to_vec(),
1540 );
1541 let color = gpui::hsla(0.0, 1.0, 0.5, 1.0);
1542 let result = into_image_source(svg, Some(color), None);
1543 assert!(result.is_some(), "colorized SVG should convert");
1544 }
1545
1546 #[derive(Debug)]
1550 struct TestCustomIcon;
1551
1552 impl native_theme::IconProvider for TestCustomIcon {
1553 fn icon_name(&self, _set: native_theme::IconSet) -> Option<&str> {
1554 None }
1556 fn icon_svg(&self, _set: native_theme::IconSet) -> Option<&'static [u8]> {
1557 Some(b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10'/></svg>")
1558 }
1559 }
1560
1561 #[derive(Debug)]
1563 struct EmptyProvider;
1564
1565 impl native_theme::IconProvider for EmptyProvider {
1566 fn icon_name(&self, _set: native_theme::IconSet) -> Option<&str> {
1567 None
1568 }
1569 fn icon_svg(&self, _set: native_theme::IconSet) -> Option<&'static [u8]> {
1570 None
1571 }
1572 }
1573
1574 #[test]
1575 fn custom_icon_to_image_source_with_svg_provider_returns_some() {
1576 let result = custom_icon_to_image_source(
1577 &TestCustomIcon,
1578 native_theme::IconSet::Material,
1579 None,
1580 None,
1581 );
1582 assert!(result.is_some());
1583 }
1584
1585 #[test]
1586 fn custom_icon_to_image_source_with_empty_provider_returns_none() {
1587 let result = custom_icon_to_image_source(
1588 &EmptyProvider,
1589 native_theme::IconSet::Material,
1590 None,
1591 None,
1592 );
1593 assert!(result.is_none());
1594 }
1595
1596 #[test]
1597 fn custom_icon_to_image_source_with_color() {
1598 let color = Hsla {
1599 h: 0.0,
1600 s: 1.0,
1601 l: 0.5,
1602 a: 1.0,
1603 };
1604 let result = custom_icon_to_image_source(
1605 &TestCustomIcon,
1606 native_theme::IconSet::Material,
1607 Some(color),
1608 None,
1609 );
1610 assert!(result.is_some());
1611 }
1612
1613 #[test]
1614 fn custom_icon_to_image_source_accepts_dyn_provider() {
1615 let boxed: Box<dyn native_theme::IconProvider> = Box::new(TestCustomIcon);
1616 let result =
1617 custom_icon_to_image_source(&*boxed, native_theme::IconSet::Material, None, None);
1618 assert!(result.is_some());
1619 }
1620
1621 #[test]
1624 fn animated_frames_returns_sources() {
1625 let anim = AnimatedIcon::Frames {
1626 frames: vec![
1627 IconData::Svg(b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='red'/></svg>".to_vec()),
1628 IconData::Svg(b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='8' fill='blue'/></svg>".to_vec()),
1629 IconData::Svg(b"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='6' fill='green'/></svg>".to_vec()),
1630 ],
1631 frame_duration_ms: 80,
1632 };
1633 let result = animated_frames_to_image_sources(&anim);
1634 let ais = result.expect("Frames variant should return Some");
1635 assert_eq!(ais.sources.len(), 3);
1636 assert_eq!(ais.frame_duration_ms, 80);
1637 }
1638
1639 #[test]
1640 fn animated_frames_transform_returns_none() {
1641 let anim = AnimatedIcon::Transform {
1642 icon: IconData::Svg(
1643 b"<svg xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10'/></svg>"
1644 .to_vec(),
1645 ),
1646 animation: native_theme::TransformAnimation::Spin { duration_ms: 1000 },
1647 };
1648 let result = animated_frames_to_image_sources(&anim);
1649 assert!(result.is_none());
1650 }
1651
1652 #[test]
1653 fn animated_frames_empty_returns_none() {
1654 let anim = AnimatedIcon::Frames {
1655 frames: vec![],
1656 frame_duration_ms: 80,
1657 };
1658 let result = animated_frames_to_image_sources(&anim);
1659 assert!(result.is_none());
1660 }
1661
1662 #[test]
1663 fn spin_animation_constructs_without_context() {
1664 let svg_element = gpui::svg();
1665 let _animated = with_spin_animation(svg_element, "test-spin", 1000);
1668 }
1669}
1670
1671#[cfg(test)]
1672#[cfg(target_os = "linux")]
1673#[allow(clippy::unwrap_used, clippy::expect_used)]
1674mod freedesktop_mapping_tests {
1675 use super::tests::ALL_ICON_NAMES;
1676 use super::*;
1677 use native_theme::LinuxDesktop;
1678
1679 #[test]
1680 fn all_86_gpui_icons_have_mapping_on_kde() {
1681 for name in ALL_ICON_NAMES {
1682 let fd_name = freedesktop_name_for_gpui_icon(name.clone(), LinuxDesktop::Kde);
1683 assert!(
1684 !fd_name.is_empty(),
1685 "Empty KDE freedesktop mapping for an IconName variant",
1686 );
1687 }
1688 }
1689
1690 #[test]
1691 fn eye_differs_by_de() {
1692 assert_eq!(
1693 freedesktop_name_for_gpui_icon(IconName::Eye, LinuxDesktop::Kde),
1694 "view-visible",
1695 );
1696 assert_eq!(
1697 freedesktop_name_for_gpui_icon(IconName::Eye, LinuxDesktop::Gnome),
1698 "view-reveal",
1699 );
1700 }
1701
1702 #[test]
1703 fn freedesktop_standard_ignores_de() {
1704 assert_eq!(
1706 freedesktop_name_for_gpui_icon(IconName::Copy, LinuxDesktop::Kde),
1707 freedesktop_name_for_gpui_icon(IconName::Copy, LinuxDesktop::Gnome),
1708 );
1709 }
1710
1711 #[test]
1712 fn all_86_gpui_icons_have_mapping_on_gnome() {
1713 for name in ALL_ICON_NAMES {
1714 let fd_name = freedesktop_name_for_gpui_icon(name.clone(), LinuxDesktop::Gnome);
1715 assert!(
1716 !fd_name.is_empty(),
1717 "Empty GNOME freedesktop mapping for an IconName variant",
1718 );
1719 }
1720 }
1721
1722 #[test]
1723 fn xfce_uses_gnome_names() {
1724 assert_eq!(
1726 freedesktop_name_for_gpui_icon(IconName::Eye, LinuxDesktop::Xfce),
1727 "view-reveal",
1728 );
1729 assert_eq!(
1730 freedesktop_name_for_gpui_icon(IconName::Bell, LinuxDesktop::Xfce),
1731 "alarm",
1732 );
1733 }
1734
1735 #[test]
1736 fn all_kde_names_resolve_in_breeze() {
1737 let theme = native_theme::system_icon_theme();
1738 if !theme.to_lowercase().contains("breeze") {
1740 eprintln!("Skipping: system theme is '{}', not Breeze", theme);
1741 return;
1742 }
1743
1744 let mut missing = Vec::new();
1745 for name in ALL_ICON_NAMES {
1746 let fd_name = freedesktop_name_for_gpui_icon(name.clone(), LinuxDesktop::Kde);
1747 if native_theme::load_freedesktop_icon_by_name(fd_name, theme, 24).is_none() {
1748 missing.push(format!("{} (not found)", fd_name));
1749 }
1750 }
1751 assert!(
1752 missing.is_empty(),
1753 "These gpui icons did not resolve in Breeze:\n {}",
1754 missing.join("\n "),
1755 );
1756 }
1757
1758 #[test]
1759 fn gnome_names_resolve_in_adwaita() {
1760 let mut missing = Vec::new();
1763 for name in ALL_ICON_NAMES {
1764 let fd_name = freedesktop_name_for_gpui_icon(name.clone(), LinuxDesktop::Gnome);
1765 if native_theme::load_freedesktop_icon_by_name(fd_name, "Adwaita", 24).is_none() {
1766 missing.push(format!("{} (not found)", fd_name));
1767 }
1768 }
1769 assert!(
1770 missing.is_empty(),
1771 "These GNOME mappings did not resolve in Adwaita:\n {}",
1772 missing.join("\n "),
1773 );
1774 }
1775}