1use std::sync::OnceLock;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum IconRole {
43 DialogWarning,
46 DialogError,
48 DialogInfo,
50 DialogQuestion,
52 DialogSuccess,
54 Shield,
56
57 WindowClose,
60 WindowMinimize,
62 WindowMaximize,
64 WindowRestore,
66
67 ActionSave,
70 ActionDelete,
72 ActionCopy,
74 ActionPaste,
76 ActionCut,
78 ActionUndo,
80 ActionRedo,
82 ActionSearch,
84 ActionSettings,
86 ActionEdit,
88 ActionAdd,
90 ActionRemove,
92 ActionRefresh,
94 ActionPrint,
96
97 NavBack,
100 NavForward,
102 NavUp,
104 NavDown,
106 NavHome,
108 NavMenu,
110
111 FileGeneric,
114 FolderClosed,
116 FolderOpen,
118 TrashEmpty,
120 TrashFull,
122
123 StatusLoading,
126 StatusCheck,
128 StatusError,
130
131 UserAccount,
134 Notification,
136 Help,
138 Lock,
140}
141
142impl IconRole {
143 pub const ALL: [IconRole; 42] = [
147 Self::DialogWarning,
149 Self::DialogError,
150 Self::DialogInfo,
151 Self::DialogQuestion,
152 Self::DialogSuccess,
153 Self::Shield,
154 Self::WindowClose,
156 Self::WindowMinimize,
157 Self::WindowMaximize,
158 Self::WindowRestore,
159 Self::ActionSave,
161 Self::ActionDelete,
162 Self::ActionCopy,
163 Self::ActionPaste,
164 Self::ActionCut,
165 Self::ActionUndo,
166 Self::ActionRedo,
167 Self::ActionSearch,
168 Self::ActionSettings,
169 Self::ActionEdit,
170 Self::ActionAdd,
171 Self::ActionRemove,
172 Self::ActionRefresh,
173 Self::ActionPrint,
174 Self::NavBack,
176 Self::NavForward,
177 Self::NavUp,
178 Self::NavDown,
179 Self::NavHome,
180 Self::NavMenu,
181 Self::FileGeneric,
183 Self::FolderClosed,
184 Self::FolderOpen,
185 Self::TrashEmpty,
186 Self::TrashFull,
187 Self::StatusLoading,
189 Self::StatusCheck,
190 Self::StatusError,
191 Self::UserAccount,
193 Self::Notification,
194 Self::Help,
195 Self::Lock,
196 ];
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
225#[non_exhaustive]
226#[must_use = "loading icon data without using it is likely a bug"]
227pub enum IconData {
228 Svg(Vec<u8>),
230
231 Rgba {
233 width: u32,
235 height: u32,
237 data: Vec<u8>,
239 },
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
265#[non_exhaustive]
266pub enum IconSet {
267 SfSymbols,
269 SegoeIcons,
271 Freedesktop,
273 Material,
275 Lucide,
277}
278
279impl IconSet {
280 pub fn from_name(name: &str) -> Option<Self> {
287 match name {
288 "sf-symbols" => Some(Self::SfSymbols),
289 "segoe-fluent" => Some(Self::SegoeIcons),
290 "freedesktop" => Some(Self::Freedesktop),
291 "material" => Some(Self::Material),
292 "lucide" => Some(Self::Lucide),
293 _ => None,
294 }
295 }
296
297 pub fn name(&self) -> &'static str {
299 match self {
300 Self::SfSymbols => "sf-symbols",
301 Self::SegoeIcons => "segoe-fluent",
302 Self::Freedesktop => "freedesktop",
303 Self::Material => "material",
304 Self::Lucide => "lucide",
305 }
306 }
307}
308
309#[allow(unreachable_patterns)] pub fn icon_name(set: IconSet, role: IconRole) -> Option<&'static str> {
326 match set {
327 IconSet::SfSymbols => sf_symbols_name(role),
328 IconSet::SegoeIcons => segoe_name(role),
329 IconSet::Freedesktop => freedesktop_name(role),
330 IconSet::Material => material_name(role),
331 IconSet::Lucide => lucide_name(role),
332 _ => None,
333 }
334}
335
336#[must_use = "this returns the current icon set for the platform"]
353pub fn system_icon_set() -> IconSet {
354 if cfg!(any(target_os = "macos", target_os = "ios")) {
355 IconSet::SfSymbols
356 } else if cfg!(target_os = "windows") {
357 IconSet::SegoeIcons
358 } else if cfg!(target_os = "linux") {
359 IconSet::Freedesktop
360 } else {
361 IconSet::Material
362 }
363}
364
365#[must_use = "this returns the current icon theme name"]
392pub fn system_icon_theme() -> String {
393 #[cfg(target_os = "linux")]
394 static CACHED_ICON_THEME: OnceLock<String> = OnceLock::new();
395
396 #[cfg(any(target_os = "macos", target_os = "ios"))]
397 {
398 return "sf-symbols".to_string();
399 }
400
401 #[cfg(target_os = "windows")]
402 {
403 return "segoe-fluent".to_string();
404 }
405
406 #[cfg(target_os = "linux")]
407 {
408 CACHED_ICON_THEME
409 .get_or_init(detect_linux_icon_theme)
410 .clone()
411 }
412
413 #[cfg(not(any(
414 target_os = "linux",
415 target_os = "windows",
416 target_os = "macos",
417 target_os = "ios"
418 )))]
419 {
420 "material".to_string()
421 }
422}
423
424#[cfg(target_os = "linux")]
426fn detect_linux_icon_theme() -> String {
427 let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
428 let de = crate::detect_linux_de(&desktop);
429
430 match de {
431 crate::LinuxDesktop::Kde => detect_kde_icon_theme(),
432 crate::LinuxDesktop::Gnome | crate::LinuxDesktop::Budgie => {
433 gsettings_icon_theme("org.gnome.desktop.interface")
434 }
435 crate::LinuxDesktop::Cinnamon => gsettings_icon_theme("org.cinnamon.desktop.interface"),
436 crate::LinuxDesktop::Xfce => detect_xfce_icon_theme(),
437 crate::LinuxDesktop::Mate => gsettings_icon_theme("org.mate.interface"),
438 crate::LinuxDesktop::LxQt => detect_lxqt_icon_theme(),
439 crate::LinuxDesktop::Unknown => {
440 let kde = detect_kde_icon_theme();
441 if kde != "hicolor" {
442 return kde;
443 }
444 let gnome = gsettings_icon_theme("org.gnome.desktop.interface");
445 if gnome != "hicolor" {
446 return gnome;
447 }
448 "hicolor".to_string()
449 }
450 }
451}
452
453#[cfg(target_os = "linux")]
461fn detect_kde_icon_theme() -> String {
462 let config_dir = xdg_config_dir();
463 let paths = [
464 config_dir.join("kdeglobals"),
465 config_dir.join("kdedefaults").join("kdeglobals"),
466 ];
467
468 for path in &paths {
469 if let Some(theme) = read_ini_value(path, "Icons", "Theme") {
470 return theme;
471 }
472 }
473 "hicolor".to_string()
474}
475
476#[cfg(target_os = "linux")]
478fn gsettings_icon_theme(schema: &str) -> String {
479 std::process::Command::new("gsettings")
480 .args(["get", schema, "icon-theme"])
481 .output()
482 .ok()
483 .filter(|o| o.status.success())
484 .and_then(|o| String::from_utf8(o.stdout).ok())
485 .map(|s| s.trim().trim_matches('\'').to_string())
486 .filter(|s| !s.is_empty())
487 .unwrap_or_else(|| "hicolor".to_string())
488}
489
490#[cfg(target_os = "linux")]
492fn detect_xfce_icon_theme() -> String {
493 std::process::Command::new("xfconf-query")
494 .args(["-c", "xsettings", "-p", "/Net/IconThemeName"])
495 .output()
496 .ok()
497 .filter(|o| o.status.success())
498 .and_then(|o| String::from_utf8(o.stdout).ok())
499 .map(|s| s.trim().to_string())
500 .filter(|s| !s.is_empty())
501 .unwrap_or_else(|| "hicolor".to_string())
502}
503
504#[cfg(target_os = "linux")]
509fn detect_lxqt_icon_theme() -> String {
510 let path = xdg_config_dir().join("lxqt").join("lxqt.conf");
511
512 if let Ok(content) = std::fs::read_to_string(&path) {
513 for line in content.lines() {
514 let trimmed = line.trim();
515 if let Some(value) = trimmed.strip_prefix("icon_theme=") {
516 let value = value.trim();
517 if !value.is_empty() {
518 return value.to_string();
519 }
520 }
521 }
522 }
523 "hicolor".to_string()
524}
525
526#[cfg(target_os = "linux")]
528fn xdg_config_dir() -> std::path::PathBuf {
529 if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME")
530 && !config_home.is_empty()
531 {
532 return std::path::PathBuf::from(config_home);
533 }
534 std::env::var("HOME")
535 .map(std::path::PathBuf::from)
536 .unwrap_or_else(|_| std::path::PathBuf::from("/tmp"))
537 .join(".config")
538}
539
540#[cfg(target_os = "linux")]
546fn read_ini_value(path: &std::path::Path, section: &str, key: &str) -> Option<String> {
547 let content = std::fs::read_to_string(path).ok()?;
548 let target_section = format!("[{}]", section);
549 let mut in_section = false;
550
551 for line in content.lines() {
552 let trimmed = line.trim();
553 if trimmed.starts_with('[') {
554 in_section = trimmed == target_section;
555 continue;
556 }
557 if in_section && let Some(value) = trimmed.strip_prefix(key) {
558 let value = value.trim_start();
559 if let Some(value) = value.strip_prefix('=') {
560 let value = value.trim();
561 if !value.is_empty() {
562 return Some(value.to_string());
563 }
564 }
565 }
566 }
567 None
568}
569
570#[allow(unreachable_patterns)]
573fn sf_symbols_name(role: IconRole) -> Option<&'static str> {
574 Some(match role {
575 IconRole::DialogWarning => "exclamationmark.triangle.fill",
577 IconRole::DialogError => "xmark.circle.fill",
578 IconRole::DialogInfo => "info.circle.fill",
579 IconRole::DialogQuestion => "questionmark.circle.fill",
580 IconRole::DialogSuccess => "checkmark.circle.fill",
581 IconRole::Shield => "shield.fill",
582
583 IconRole::WindowClose => "xmark",
585 IconRole::WindowMinimize => "minus",
586 IconRole::WindowMaximize => "arrow.up.left.and.arrow.down.right",
587 IconRole::WindowRestore => return None,
589
590 IconRole::ActionSave => "square.and.arrow.down",
592 IconRole::ActionDelete => "trash",
593 IconRole::ActionCopy => "doc.on.doc",
594 IconRole::ActionPaste => "doc.on.clipboard",
595 IconRole::ActionCut => "scissors",
596 IconRole::ActionUndo => "arrow.uturn.backward",
597 IconRole::ActionRedo => "arrow.uturn.forward",
598 IconRole::ActionSearch => "magnifyingglass",
599 IconRole::ActionSettings => "gearshape",
600 IconRole::ActionEdit => "pencil",
601 IconRole::ActionAdd => "plus",
602 IconRole::ActionRemove => "minus",
603 IconRole::ActionRefresh => "arrow.clockwise",
604 IconRole::ActionPrint => "printer",
605
606 IconRole::NavBack => "chevron.backward",
608 IconRole::NavForward => "chevron.forward",
609 IconRole::NavUp => "chevron.up",
610 IconRole::NavDown => "chevron.down",
611 IconRole::NavHome => "house",
612 IconRole::NavMenu => "line.horizontal.3",
613
614 IconRole::FileGeneric => "doc",
616 IconRole::FolderClosed => "folder",
617 IconRole::FolderOpen => return None,
619 IconRole::TrashEmpty => "trash",
620 IconRole::TrashFull => return None,
622
623 IconRole::StatusLoading => return None,
626 IconRole::StatusCheck => "checkmark",
627 IconRole::StatusError => "xmark.circle.fill",
628
629 IconRole::UserAccount => "person.fill",
631 IconRole::Notification => "bell.fill",
632 IconRole::Help => "questionmark.circle",
633 IconRole::Lock => "lock.fill",
634
635 _ => return None,
636 })
637}
638
639#[allow(unreachable_patterns)]
640fn segoe_name(role: IconRole) -> Option<&'static str> {
641 Some(match role {
642 IconRole::DialogWarning => "SIID_WARNING",
644 IconRole::DialogError => "SIID_ERROR",
645 IconRole::DialogInfo => "SIID_INFO",
646 IconRole::DialogQuestion => "IDI_QUESTION",
647 IconRole::DialogSuccess => return None,
649 IconRole::Shield => "SIID_SHIELD",
650
651 IconRole::WindowClose => "ChromeClose",
653 IconRole::WindowMinimize => "ChromeMinimize",
654 IconRole::WindowMaximize => "ChromeMaximize",
655 IconRole::WindowRestore => "ChromeRestore",
656
657 IconRole::ActionSave => "Save",
659 IconRole::ActionDelete => "SIID_DELETE",
660 IconRole::ActionCopy => "Copy",
661 IconRole::ActionPaste => "Paste",
662 IconRole::ActionCut => "Cut",
663 IconRole::ActionUndo => "Undo",
664 IconRole::ActionRedo => "Redo",
665 IconRole::ActionSearch => "SIID_FIND",
666 IconRole::ActionSettings => "SIID_SETTINGS",
667 IconRole::ActionEdit => "Edit",
668 IconRole::ActionAdd => "Add",
669 IconRole::ActionRemove => "Remove",
670 IconRole::ActionRefresh => "Refresh",
671 IconRole::ActionPrint => "SIID_PRINTER",
672
673 IconRole::NavBack => "Back",
675 IconRole::NavForward => "Forward",
676 IconRole::NavUp => "Up",
677 IconRole::NavDown => "Down",
678 IconRole::NavHome => "Home",
679 IconRole::NavMenu => "GlobalNavigationButton",
680
681 IconRole::FileGeneric => "SIID_DOCNOASSOC",
683 IconRole::FolderClosed => "SIID_FOLDER",
684 IconRole::FolderOpen => "SIID_FOLDEROPEN",
685 IconRole::TrashEmpty => "SIID_RECYCLER",
686 IconRole::TrashFull => "SIID_RECYCLERFULL",
687
688 IconRole::StatusLoading => return None,
691 IconRole::StatusCheck => "CheckMark",
692 IconRole::StatusError => "SIID_ERROR",
693
694 IconRole::UserAccount => "SIID_USERS",
696 IconRole::Notification => "Ringer",
697 IconRole::Help => "SIID_HELP",
698 IconRole::Lock => "SIID_LOCK",
699
700 _ => return None,
701 })
702}
703
704#[allow(unreachable_patterns)]
705fn freedesktop_name(role: IconRole) -> Option<&'static str> {
706 Some(match role {
707 IconRole::DialogWarning => "dialog-warning",
709 IconRole::DialogError => "dialog-error",
710 IconRole::DialogInfo => "dialog-information",
711 IconRole::DialogQuestion => "dialog-question",
712 IconRole::DialogSuccess => "emblem-ok-symbolic",
713 IconRole::Shield => "security-high",
714
715 IconRole::WindowClose => "window-close",
717 IconRole::WindowMinimize => "window-minimize",
718 IconRole::WindowMaximize => "window-maximize",
719 IconRole::WindowRestore => "window-restore",
720
721 IconRole::ActionSave => "document-save",
723 IconRole::ActionDelete => "edit-delete",
724 IconRole::ActionCopy => "edit-copy",
725 IconRole::ActionPaste => "edit-paste",
726 IconRole::ActionCut => "edit-cut",
727 IconRole::ActionUndo => "edit-undo",
728 IconRole::ActionRedo => "edit-redo",
729 IconRole::ActionSearch => "edit-find",
730 IconRole::ActionSettings => "preferences-system",
731 IconRole::ActionEdit => "document-edit",
732 IconRole::ActionAdd => "list-add",
733 IconRole::ActionRemove => "list-remove",
734 IconRole::ActionRefresh => "view-refresh",
735 IconRole::ActionPrint => "document-print",
736
737 IconRole::NavBack => "go-previous",
739 IconRole::NavForward => "go-next",
740 IconRole::NavUp => "go-up",
741 IconRole::NavDown => "go-down",
742 IconRole::NavHome => "go-home",
743 IconRole::NavMenu => "open-menu",
744
745 IconRole::FileGeneric => "text-x-generic",
747 IconRole::FolderClosed => "folder",
748 IconRole::FolderOpen => "folder-open",
749 IconRole::TrashEmpty => "user-trash",
750 IconRole::TrashFull => "user-trash-full",
751
752 IconRole::StatusLoading => "process-working",
754 IconRole::StatusCheck => "emblem-default",
755 IconRole::StatusError => "dialog-error",
756
757 IconRole::UserAccount => "system-users",
759 IconRole::Notification => return None,
761 IconRole::Help => "help-browser",
762 IconRole::Lock => "system-lock-screen",
763
764 _ => return None,
765 })
766}
767
768#[allow(unreachable_patterns)]
769fn material_name(role: IconRole) -> Option<&'static str> {
770 Some(match role {
771 IconRole::DialogWarning => "warning",
773 IconRole::DialogError => "error",
774 IconRole::DialogInfo => "info",
775 IconRole::DialogQuestion => "help",
776 IconRole::DialogSuccess => "check_circle",
777 IconRole::Shield => "shield",
778
779 IconRole::WindowClose => "close",
781 IconRole::WindowMinimize => "minimize",
782 IconRole::WindowMaximize => "open_in_full",
783 IconRole::WindowRestore => "close_fullscreen",
784
785 IconRole::ActionSave => "save",
787 IconRole::ActionDelete => "delete",
788 IconRole::ActionCopy => "content_copy",
789 IconRole::ActionPaste => "content_paste",
790 IconRole::ActionCut => "content_cut",
791 IconRole::ActionUndo => "undo",
792 IconRole::ActionRedo => "redo",
793 IconRole::ActionSearch => "search",
794 IconRole::ActionSettings => "settings",
795 IconRole::ActionEdit => "edit",
796 IconRole::ActionAdd => "add",
797 IconRole::ActionRemove => "remove",
798 IconRole::ActionRefresh => "refresh",
799 IconRole::ActionPrint => "print",
800
801 IconRole::NavBack => "arrow_back",
803 IconRole::NavForward => "arrow_forward",
804 IconRole::NavUp => "arrow_upward",
805 IconRole::NavDown => "arrow_downward",
806 IconRole::NavHome => "home",
807 IconRole::NavMenu => "menu",
808
809 IconRole::FileGeneric => "description",
811 IconRole::FolderClosed => "folder",
812 IconRole::FolderOpen => "folder_open",
813 IconRole::TrashEmpty => "delete",
814 IconRole::TrashFull => return None,
816
817 IconRole::StatusLoading => "progress_activity",
819 IconRole::StatusCheck => "check",
820 IconRole::StatusError => "error",
821
822 IconRole::UserAccount => "person",
824 IconRole::Notification => "notifications",
825 IconRole::Help => "help",
826 IconRole::Lock => "lock",
827
828 _ => return None,
829 })
830}
831
832#[allow(unreachable_patterns)]
833fn lucide_name(role: IconRole) -> Option<&'static str> {
834 Some(match role {
835 IconRole::DialogWarning => "triangle-alert",
837 IconRole::DialogError => "circle-x",
838 IconRole::DialogInfo => "info",
839 IconRole::DialogQuestion => "circle-question-mark",
840 IconRole::DialogSuccess => "circle-check",
841 IconRole::Shield => "shield",
842
843 IconRole::WindowClose => "x",
845 IconRole::WindowMinimize => "minimize",
846 IconRole::WindowMaximize => "maximize",
847 IconRole::WindowRestore => "minimize-2",
848
849 IconRole::ActionSave => "save",
851 IconRole::ActionDelete => "trash-2",
852 IconRole::ActionCopy => "copy",
853 IconRole::ActionPaste => "clipboard-paste",
854 IconRole::ActionCut => "scissors",
855 IconRole::ActionUndo => "undo-2",
856 IconRole::ActionRedo => "redo-2",
857 IconRole::ActionSearch => "search",
858 IconRole::ActionSettings => "settings",
859 IconRole::ActionEdit => "pencil",
860 IconRole::ActionAdd => "plus",
861 IconRole::ActionRemove => "minus",
862 IconRole::ActionRefresh => "refresh-cw",
863 IconRole::ActionPrint => "printer",
864
865 IconRole::NavBack => "chevron-left",
867 IconRole::NavForward => "chevron-right",
868 IconRole::NavUp => "chevron-up",
869 IconRole::NavDown => "chevron-down",
870 IconRole::NavHome => "house",
871 IconRole::NavMenu => "menu",
872
873 IconRole::FileGeneric => "file",
875 IconRole::FolderClosed => "folder-closed",
876 IconRole::FolderOpen => "folder-open",
877 IconRole::TrashEmpty => "trash-2",
878 IconRole::TrashFull => return None,
880
881 IconRole::StatusLoading => "loader",
883 IconRole::StatusCheck => "check",
884 IconRole::StatusError => "circle-x",
885
886 IconRole::UserAccount => "user",
888 IconRole::Notification => "bell",
889 IconRole::Help => "circle-question-mark",
890 IconRole::Lock => "lock",
891
892 _ => return None,
893 })
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899
900 #[test]
903 fn icon_role_all_has_42_variants() {
904 assert_eq!(IconRole::ALL.len(), 42);
905 }
906
907 #[test]
908 fn icon_role_all_contains_every_variant() {
909 let all = &IconRole::ALL;
911
912 assert!(all.contains(&IconRole::DialogWarning));
914 assert!(all.contains(&IconRole::DialogError));
915 assert!(all.contains(&IconRole::DialogInfo));
916 assert!(all.contains(&IconRole::DialogQuestion));
917 assert!(all.contains(&IconRole::DialogSuccess));
918 assert!(all.contains(&IconRole::Shield));
919
920 assert!(all.contains(&IconRole::WindowClose));
922 assert!(all.contains(&IconRole::WindowMinimize));
923 assert!(all.contains(&IconRole::WindowMaximize));
924 assert!(all.contains(&IconRole::WindowRestore));
925
926 assert!(all.contains(&IconRole::ActionSave));
928 assert!(all.contains(&IconRole::ActionDelete));
929 assert!(all.contains(&IconRole::ActionCopy));
930 assert!(all.contains(&IconRole::ActionPaste));
931 assert!(all.contains(&IconRole::ActionCut));
932 assert!(all.contains(&IconRole::ActionUndo));
933 assert!(all.contains(&IconRole::ActionRedo));
934 assert!(all.contains(&IconRole::ActionSearch));
935 assert!(all.contains(&IconRole::ActionSettings));
936 assert!(all.contains(&IconRole::ActionEdit));
937 assert!(all.contains(&IconRole::ActionAdd));
938 assert!(all.contains(&IconRole::ActionRemove));
939 assert!(all.contains(&IconRole::ActionRefresh));
940 assert!(all.contains(&IconRole::ActionPrint));
941
942 assert!(all.contains(&IconRole::NavBack));
944 assert!(all.contains(&IconRole::NavForward));
945 assert!(all.contains(&IconRole::NavUp));
946 assert!(all.contains(&IconRole::NavDown));
947 assert!(all.contains(&IconRole::NavHome));
948 assert!(all.contains(&IconRole::NavMenu));
949
950 assert!(all.contains(&IconRole::FileGeneric));
952 assert!(all.contains(&IconRole::FolderClosed));
953 assert!(all.contains(&IconRole::FolderOpen));
954 assert!(all.contains(&IconRole::TrashEmpty));
955 assert!(all.contains(&IconRole::TrashFull));
956
957 assert!(all.contains(&IconRole::StatusLoading));
959 assert!(all.contains(&IconRole::StatusCheck));
960 assert!(all.contains(&IconRole::StatusError));
961
962 assert!(all.contains(&IconRole::UserAccount));
964 assert!(all.contains(&IconRole::Notification));
965 assert!(all.contains(&IconRole::Help));
966 assert!(all.contains(&IconRole::Lock));
967 }
968
969 #[test]
970 fn icon_role_all_no_duplicates() {
971 let all = &IconRole::ALL;
972 for (i, role) in all.iter().enumerate() {
973 for (j, other) in all.iter().enumerate() {
974 if i != j {
975 assert_ne!(role, other, "Duplicate at index {i} and {j}");
976 }
977 }
978 }
979 }
980
981 #[test]
982 fn icon_role_derives_copy_clone() {
983 let role = IconRole::ActionCopy;
984 let copied1 = role;
985 let copied2 = role;
986 assert_eq!(role, copied1);
987 assert_eq!(role, copied2);
988 }
989
990 #[test]
991 fn icon_role_derives_debug() {
992 let s = format!("{:?}", IconRole::DialogWarning);
993 assert!(s.contains("DialogWarning"));
994 }
995
996 #[test]
997 fn icon_role_derives_hash() {
998 use std::collections::HashSet;
999 let mut set = HashSet::new();
1000 set.insert(IconRole::ActionSave);
1001 set.insert(IconRole::ActionDelete);
1002 assert_eq!(set.len(), 2);
1003 assert!(set.contains(&IconRole::ActionSave));
1004 }
1005
1006 #[test]
1009 fn icon_data_svg_construct_and_match() {
1010 let svg_bytes = b"<svg></svg>".to_vec();
1011 let data = IconData::Svg(svg_bytes.clone());
1012 match data {
1013 IconData::Svg(bytes) => assert_eq!(bytes, svg_bytes),
1014 _ => panic!("Expected Svg variant"),
1015 }
1016 }
1017
1018 #[test]
1019 fn icon_data_rgba_construct_and_match() {
1020 let pixels = vec![255, 0, 0, 255]; let data = IconData::Rgba {
1022 width: 1,
1023 height: 1,
1024 data: pixels.clone(),
1025 };
1026 match data {
1027 IconData::Rgba {
1028 width,
1029 height,
1030 data,
1031 } => {
1032 assert_eq!(width, 1);
1033 assert_eq!(height, 1);
1034 assert_eq!(data, pixels);
1035 }
1036 _ => panic!("Expected Rgba variant"),
1037 }
1038 }
1039
1040 #[test]
1041 fn icon_data_derives_debug() {
1042 let data = IconData::Svg(vec![]);
1043 let s = format!("{:?}", data);
1044 assert!(s.contains("Svg"));
1045 }
1046
1047 #[test]
1048 fn icon_data_derives_clone() {
1049 let data = IconData::Rgba {
1050 width: 16,
1051 height: 16,
1052 data: vec![0; 16 * 16 * 4],
1053 };
1054 let cloned = data.clone();
1055 assert_eq!(data, cloned);
1056 }
1057
1058 #[test]
1059 fn icon_data_derives_eq() {
1060 let a = IconData::Svg(b"<svg/>".to_vec());
1061 let b = IconData::Svg(b"<svg/>".to_vec());
1062 assert_eq!(a, b);
1063
1064 let c = IconData::Svg(b"<other/>".to_vec());
1065 assert_ne!(a, c);
1066 }
1067
1068 #[test]
1071 fn icon_set_from_name_sf_symbols() {
1072 assert_eq!(IconSet::from_name("sf-symbols"), Some(IconSet::SfSymbols));
1073 }
1074
1075 #[test]
1076 fn icon_set_from_name_segoe_fluent() {
1077 assert_eq!(
1078 IconSet::from_name("segoe-fluent"),
1079 Some(IconSet::SegoeIcons)
1080 );
1081 }
1082
1083 #[test]
1084 fn icon_set_from_name_freedesktop() {
1085 assert_eq!(
1086 IconSet::from_name("freedesktop"),
1087 Some(IconSet::Freedesktop)
1088 );
1089 }
1090
1091 #[test]
1092 fn icon_set_from_name_material() {
1093 assert_eq!(IconSet::from_name("material"), Some(IconSet::Material));
1094 }
1095
1096 #[test]
1097 fn icon_set_from_name_lucide() {
1098 assert_eq!(IconSet::from_name("lucide"), Some(IconSet::Lucide));
1099 }
1100
1101 #[test]
1102 fn icon_set_from_name_unknown() {
1103 assert_eq!(IconSet::from_name("unknown"), None);
1104 }
1105
1106 #[test]
1107 fn icon_set_name_sf_symbols() {
1108 assert_eq!(IconSet::SfSymbols.name(), "sf-symbols");
1109 }
1110
1111 #[test]
1112 fn icon_set_name_segoe_fluent() {
1113 assert_eq!(IconSet::SegoeIcons.name(), "segoe-fluent");
1114 }
1115
1116 #[test]
1117 fn icon_set_name_freedesktop() {
1118 assert_eq!(IconSet::Freedesktop.name(), "freedesktop");
1119 }
1120
1121 #[test]
1122 fn icon_set_name_material() {
1123 assert_eq!(IconSet::Material.name(), "material");
1124 }
1125
1126 #[test]
1127 fn icon_set_name_lucide() {
1128 assert_eq!(IconSet::Lucide.name(), "lucide");
1129 }
1130
1131 #[test]
1132 fn icon_set_from_name_name_round_trip() {
1133 let sets = [
1134 IconSet::SfSymbols,
1135 IconSet::SegoeIcons,
1136 IconSet::Freedesktop,
1137 IconSet::Material,
1138 IconSet::Lucide,
1139 ];
1140 for set in &sets {
1141 let name = set.name();
1142 let parsed = IconSet::from_name(name);
1143 assert_eq!(parsed, Some(*set), "Round-trip failed for {:?}", set);
1144 }
1145 }
1146
1147 #[test]
1148 fn icon_set_derives_copy_clone() {
1149 let set = IconSet::Material;
1150 let copied1 = set;
1151 let copied2 = set;
1152 assert_eq!(set, copied1);
1153 assert_eq!(set, copied2);
1154 }
1155
1156 #[test]
1157 fn icon_set_derives_hash() {
1158 use std::collections::HashSet;
1159 let mut map = HashSet::new();
1160 map.insert(IconSet::SfSymbols);
1161 map.insert(IconSet::Lucide);
1162 assert_eq!(map.len(), 2);
1163 }
1164
1165 #[test]
1166 fn icon_set_derives_debug() {
1167 let s = format!("{:?}", IconSet::Freedesktop);
1168 assert!(s.contains("Freedesktop"));
1169 }
1170
1171 #[test]
1172 fn icon_set_serde_round_trip() {
1173 let set = IconSet::SfSymbols;
1174 let json = serde_json::to_string(&set).unwrap();
1175 let deserialized: IconSet = serde_json::from_str(&json).unwrap();
1176 assert_eq!(set, deserialized);
1177 }
1178
1179 #[test]
1182 fn icon_name_sf_symbols_action_copy() {
1183 assert_eq!(
1184 icon_name(IconSet::SfSymbols, IconRole::ActionCopy),
1185 Some("doc.on.doc")
1186 );
1187 }
1188
1189 #[test]
1190 fn icon_name_segoe_action_copy() {
1191 assert_eq!(
1192 icon_name(IconSet::SegoeIcons, IconRole::ActionCopy),
1193 Some("Copy")
1194 );
1195 }
1196
1197 #[test]
1198 fn icon_name_freedesktop_action_copy() {
1199 assert_eq!(
1200 icon_name(IconSet::Freedesktop, IconRole::ActionCopy),
1201 Some("edit-copy")
1202 );
1203 }
1204
1205 #[test]
1206 fn icon_name_material_action_copy() {
1207 assert_eq!(
1208 icon_name(IconSet::Material, IconRole::ActionCopy),
1209 Some("content_copy")
1210 );
1211 }
1212
1213 #[test]
1214 fn icon_name_lucide_action_copy() {
1215 assert_eq!(
1216 icon_name(IconSet::Lucide, IconRole::ActionCopy),
1217 Some("copy")
1218 );
1219 }
1220
1221 #[test]
1222 fn icon_name_sf_symbols_dialog_warning() {
1223 assert_eq!(
1224 icon_name(IconSet::SfSymbols, IconRole::DialogWarning),
1225 Some("exclamationmark.triangle.fill")
1226 );
1227 }
1228
1229 #[test]
1231 fn icon_name_sf_symbols_folder_open_is_none() {
1232 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::FolderOpen), None);
1233 }
1234
1235 #[test]
1236 fn icon_name_sf_symbols_trash_full_is_none() {
1237 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::TrashFull), None);
1238 }
1239
1240 #[test]
1241 fn icon_name_sf_symbols_status_loading_is_none() {
1242 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::StatusLoading), None);
1243 }
1244
1245 #[test]
1246 fn icon_name_sf_symbols_window_restore_is_none() {
1247 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::WindowRestore), None);
1248 }
1249
1250 #[test]
1251 fn icon_name_segoe_dialog_success_is_none() {
1252 assert_eq!(
1253 icon_name(IconSet::SegoeIcons, IconRole::DialogSuccess),
1254 None
1255 );
1256 }
1257
1258 #[test]
1259 fn icon_name_segoe_status_loading_is_none() {
1260 assert_eq!(
1261 icon_name(IconSet::SegoeIcons, IconRole::StatusLoading),
1262 None
1263 );
1264 }
1265
1266 #[test]
1267 fn icon_name_freedesktop_notification_is_none() {
1268 assert_eq!(
1269 icon_name(IconSet::Freedesktop, IconRole::Notification),
1270 None
1271 );
1272 }
1273
1274 #[test]
1275 fn icon_name_material_trash_full_is_none() {
1276 assert_eq!(icon_name(IconSet::Material, IconRole::TrashFull), None);
1277 }
1278
1279 #[test]
1280 fn icon_name_lucide_trash_full_is_none() {
1281 assert_eq!(icon_name(IconSet::Lucide, IconRole::TrashFull), None);
1282 }
1283
1284 #[test]
1286 fn icon_name_spot_check_dialog_error() {
1287 assert_eq!(
1288 icon_name(IconSet::SfSymbols, IconRole::DialogError),
1289 Some("xmark.circle.fill")
1290 );
1291 assert_eq!(
1292 icon_name(IconSet::SegoeIcons, IconRole::DialogError),
1293 Some("SIID_ERROR")
1294 );
1295 assert_eq!(
1296 icon_name(IconSet::Freedesktop, IconRole::DialogError),
1297 Some("dialog-error")
1298 );
1299 assert_eq!(
1300 icon_name(IconSet::Material, IconRole::DialogError),
1301 Some("error")
1302 );
1303 assert_eq!(
1304 icon_name(IconSet::Lucide, IconRole::DialogError),
1305 Some("circle-x")
1306 );
1307 }
1308
1309 #[test]
1310 fn icon_name_spot_check_nav_home() {
1311 assert_eq!(
1312 icon_name(IconSet::SfSymbols, IconRole::NavHome),
1313 Some("house")
1314 );
1315 assert_eq!(
1316 icon_name(IconSet::SegoeIcons, IconRole::NavHome),
1317 Some("Home")
1318 );
1319 assert_eq!(
1320 icon_name(IconSet::Freedesktop, IconRole::NavHome),
1321 Some("go-home")
1322 );
1323 assert_eq!(
1324 icon_name(IconSet::Material, IconRole::NavHome),
1325 Some("home")
1326 );
1327 assert_eq!(icon_name(IconSet::Lucide, IconRole::NavHome), Some("house"));
1328 }
1329
1330 #[test]
1332 fn icon_name_sf_symbols_expected_count() {
1333 let some_count = IconRole::ALL
1335 .iter()
1336 .filter(|r| icon_name(IconSet::SfSymbols, **r).is_some())
1337 .count();
1338 assert_eq!(some_count, 38, "SF Symbols should have 38 mappings");
1339 }
1340
1341 #[test]
1342 fn icon_name_segoe_expected_count() {
1343 let some_count = IconRole::ALL
1345 .iter()
1346 .filter(|r| icon_name(IconSet::SegoeIcons, **r).is_some())
1347 .count();
1348 assert_eq!(some_count, 40, "Segoe Icons should have 40 mappings");
1349 }
1350
1351 #[test]
1352 fn icon_name_freedesktop_expected_count() {
1353 let some_count = IconRole::ALL
1355 .iter()
1356 .filter(|r| icon_name(IconSet::Freedesktop, **r).is_some())
1357 .count();
1358 assert_eq!(some_count, 41, "Freedesktop should have 41 mappings");
1359 }
1360
1361 #[test]
1362 fn icon_name_material_expected_count() {
1363 let some_count = IconRole::ALL
1365 .iter()
1366 .filter(|r| icon_name(IconSet::Material, **r).is_some())
1367 .count();
1368 assert_eq!(some_count, 41, "Material should have 41 mappings");
1369 }
1370
1371 #[test]
1372 fn icon_name_lucide_expected_count() {
1373 let some_count = IconRole::ALL
1375 .iter()
1376 .filter(|r| icon_name(IconSet::Lucide, **r).is_some())
1377 .count();
1378 assert_eq!(some_count, 41, "Lucide should have 41 mappings");
1379 }
1380
1381 #[test]
1384 fn system_icon_set_returns_freedesktop_on_linux() {
1385 assert_eq!(system_icon_set(), IconSet::Freedesktop);
1387 }
1388
1389 #[test]
1390 fn system_icon_theme_returns_non_empty() {
1391 let theme = system_icon_theme();
1392 assert!(
1393 !theme.is_empty(),
1394 "system_icon_theme() should return a non-empty string"
1395 );
1396 }
1397}