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 StatusBusy,
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::StatusBusy,
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
309pub trait IconProvider: std::fmt::Debug {
351 fn icon_name(&self, set: IconSet) -> Option<&str>;
353
354 fn icon_svg(&self, set: IconSet) -> Option<&'static [u8]>;
356}
357
358impl IconProvider for IconRole {
359 fn icon_name(&self, set: IconSet) -> Option<&str> {
360 icon_name(set, *self)
361 }
362
363 fn icon_svg(&self, set: IconSet) -> Option<&'static [u8]> {
364 crate::model::bundled::bundled_icon_svg(set, *self)
365 }
366}
367
368#[allow(unreachable_patterns)] pub fn icon_name(set: IconSet, role: IconRole) -> Option<&'static str> {
385 match set {
386 IconSet::SfSymbols => sf_symbols_name(role),
387 IconSet::SegoeIcons => segoe_name(role),
388 IconSet::Freedesktop => freedesktop_name(role),
389 IconSet::Material => material_name(role),
390 IconSet::Lucide => lucide_name(role),
391 _ => None,
392 }
393}
394
395#[must_use = "this returns the current icon set for the platform"]
412pub fn system_icon_set() -> IconSet {
413 if cfg!(any(target_os = "macos", target_os = "ios")) {
414 IconSet::SfSymbols
415 } else if cfg!(target_os = "windows") {
416 IconSet::SegoeIcons
417 } else if cfg!(target_os = "linux") {
418 IconSet::Freedesktop
419 } else {
420 IconSet::Material
421 }
422}
423
424#[must_use = "this returns the current icon theme name"]
451pub fn system_icon_theme() -> String {
452 #[cfg(target_os = "linux")]
453 static CACHED_ICON_THEME: OnceLock<String> = OnceLock::new();
454
455 #[cfg(any(target_os = "macos", target_os = "ios"))]
456 {
457 return "sf-symbols".to_string();
458 }
459
460 #[cfg(target_os = "windows")]
461 {
462 return "segoe-fluent".to_string();
463 }
464
465 #[cfg(target_os = "linux")]
466 {
467 CACHED_ICON_THEME
468 .get_or_init(detect_linux_icon_theme)
469 .clone()
470 }
471
472 #[cfg(not(any(
473 target_os = "linux",
474 target_os = "windows",
475 target_os = "macos",
476 target_os = "ios"
477 )))]
478 {
479 "material".to_string()
480 }
481}
482
483#[cfg(target_os = "linux")]
485fn detect_linux_icon_theme() -> String {
486 let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
487 let de = crate::detect_linux_de(&desktop);
488
489 match de {
490 crate::LinuxDesktop::Kde => detect_kde_icon_theme(),
491 crate::LinuxDesktop::Gnome | crate::LinuxDesktop::Budgie => {
492 gsettings_icon_theme("org.gnome.desktop.interface")
493 }
494 crate::LinuxDesktop::Cinnamon => gsettings_icon_theme("org.cinnamon.desktop.interface"),
495 crate::LinuxDesktop::Xfce => detect_xfce_icon_theme(),
496 crate::LinuxDesktop::Mate => gsettings_icon_theme("org.mate.interface"),
497 crate::LinuxDesktop::LxQt => detect_lxqt_icon_theme(),
498 crate::LinuxDesktop::Unknown => {
499 let kde = detect_kde_icon_theme();
500 if kde != "hicolor" {
501 return kde;
502 }
503 let gnome = gsettings_icon_theme("org.gnome.desktop.interface");
504 if gnome != "hicolor" {
505 return gnome;
506 }
507 "hicolor".to_string()
508 }
509 }
510}
511
512#[cfg(target_os = "linux")]
520fn detect_kde_icon_theme() -> String {
521 let config_dir = xdg_config_dir();
522 let paths = [
523 config_dir.join("kdeglobals"),
524 config_dir.join("kdedefaults").join("kdeglobals"),
525 ];
526
527 for path in &paths {
528 if let Some(theme) = read_ini_value(path, "Icons", "Theme") {
529 return theme;
530 }
531 }
532 "hicolor".to_string()
533}
534
535#[cfg(target_os = "linux")]
537fn gsettings_icon_theme(schema: &str) -> String {
538 std::process::Command::new("gsettings")
539 .args(["get", schema, "icon-theme"])
540 .output()
541 .ok()
542 .filter(|o| o.status.success())
543 .and_then(|o| String::from_utf8(o.stdout).ok())
544 .map(|s| s.trim().trim_matches('\'').to_string())
545 .filter(|s| !s.is_empty())
546 .unwrap_or_else(|| "hicolor".to_string())
547}
548
549#[cfg(target_os = "linux")]
551fn detect_xfce_icon_theme() -> String {
552 std::process::Command::new("xfconf-query")
553 .args(["-c", "xsettings", "-p", "/Net/IconThemeName"])
554 .output()
555 .ok()
556 .filter(|o| o.status.success())
557 .and_then(|o| String::from_utf8(o.stdout).ok())
558 .map(|s| s.trim().to_string())
559 .filter(|s| !s.is_empty())
560 .unwrap_or_else(|| "hicolor".to_string())
561}
562
563#[cfg(target_os = "linux")]
568fn detect_lxqt_icon_theme() -> String {
569 let path = xdg_config_dir().join("lxqt").join("lxqt.conf");
570
571 if let Ok(content) = std::fs::read_to_string(&path) {
572 for line in content.lines() {
573 let trimmed = line.trim();
574 if let Some(value) = trimmed.strip_prefix("icon_theme=") {
575 let value = value.trim();
576 if !value.is_empty() {
577 return value.to_string();
578 }
579 }
580 }
581 }
582 "hicolor".to_string()
583}
584
585#[cfg(target_os = "linux")]
587fn xdg_config_dir() -> std::path::PathBuf {
588 if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME")
589 && !config_home.is_empty()
590 {
591 return std::path::PathBuf::from(config_home);
592 }
593 std::env::var("HOME")
594 .map(std::path::PathBuf::from)
595 .unwrap_or_else(|_| std::path::PathBuf::from("/tmp"))
596 .join(".config")
597}
598
599#[cfg(target_os = "linux")]
605fn read_ini_value(path: &std::path::Path, section: &str, key: &str) -> Option<String> {
606 let content = std::fs::read_to_string(path).ok()?;
607 let target_section = format!("[{}]", section);
608 let mut in_section = false;
609
610 for line in content.lines() {
611 let trimmed = line.trim();
612 if trimmed.starts_with('[') {
613 in_section = trimmed == target_section;
614 continue;
615 }
616 if in_section && let Some(value) = trimmed.strip_prefix(key) {
617 let value = value.trim_start();
618 if let Some(value) = value.strip_prefix('=') {
619 let value = value.trim();
620 if !value.is_empty() {
621 return Some(value.to_string());
622 }
623 }
624 }
625 }
626 None
627}
628
629#[allow(unreachable_patterns)]
632fn sf_symbols_name(role: IconRole) -> Option<&'static str> {
633 Some(match role {
634 IconRole::DialogWarning => "exclamationmark.triangle.fill",
636 IconRole::DialogError => "xmark.circle.fill",
637 IconRole::DialogInfo => "info.circle.fill",
638 IconRole::DialogQuestion => "questionmark.circle.fill",
639 IconRole::DialogSuccess => "checkmark.circle.fill",
640 IconRole::Shield => "shield.fill",
641
642 IconRole::WindowClose => "xmark",
644 IconRole::WindowMinimize => "minus",
645 IconRole::WindowMaximize => "arrow.up.left.and.arrow.down.right",
646 IconRole::WindowRestore => "arrow.down.right.and.arrow.up.left",
647
648 IconRole::ActionSave => "square.and.arrow.down",
650 IconRole::ActionDelete => "trash",
651 IconRole::ActionCopy => "doc.on.doc",
652 IconRole::ActionPaste => "doc.on.clipboard",
653 IconRole::ActionCut => "scissors",
654 IconRole::ActionUndo => "arrow.uturn.backward",
655 IconRole::ActionRedo => "arrow.uturn.forward",
656 IconRole::ActionSearch => "magnifyingglass",
657 IconRole::ActionSettings => "gearshape",
658 IconRole::ActionEdit => "pencil",
659 IconRole::ActionAdd => "plus",
660 IconRole::ActionRemove => "minus",
661 IconRole::ActionRefresh => "arrow.clockwise",
662 IconRole::ActionPrint => "printer",
663
664 IconRole::NavBack => "chevron.backward",
666 IconRole::NavForward => "chevron.forward",
667 IconRole::NavUp => "chevron.up",
668 IconRole::NavDown => "chevron.down",
669 IconRole::NavHome => "house",
670 IconRole::NavMenu => "line.horizontal.3",
671
672 IconRole::FileGeneric => "doc",
674 IconRole::FolderClosed => "folder",
675 IconRole::FolderOpen => return None,
677 IconRole::TrashEmpty => "trash",
678 IconRole::TrashFull => "trash.fill",
679
680 IconRole::StatusBusy => return None,
683 IconRole::StatusCheck => "checkmark",
684 IconRole::StatusError => "xmark.circle.fill",
685
686 IconRole::UserAccount => "person.fill",
688 IconRole::Notification => "bell.fill",
689 IconRole::Help => "questionmark.circle",
690 IconRole::Lock => "lock.fill",
691
692 _ => return None,
693 })
694}
695
696#[allow(unreachable_patterns)]
697fn segoe_name(role: IconRole) -> Option<&'static str> {
698 Some(match role {
699 IconRole::DialogWarning => "SIID_WARNING",
701 IconRole::DialogError => "SIID_ERROR",
702 IconRole::DialogInfo => "SIID_INFO",
703 IconRole::DialogQuestion => "IDI_QUESTION",
704 IconRole::DialogSuccess => "CheckMark",
705 IconRole::Shield => "SIID_SHIELD",
706
707 IconRole::WindowClose => "ChromeClose",
709 IconRole::WindowMinimize => "ChromeMinimize",
710 IconRole::WindowMaximize => "ChromeMaximize",
711 IconRole::WindowRestore => "ChromeRestore",
712
713 IconRole::ActionSave => "Save",
715 IconRole::ActionDelete => "SIID_DELETE",
716 IconRole::ActionCopy => "Copy",
717 IconRole::ActionPaste => "Paste",
718 IconRole::ActionCut => "Cut",
719 IconRole::ActionUndo => "Undo",
720 IconRole::ActionRedo => "Redo",
721 IconRole::ActionSearch => "SIID_FIND",
722 IconRole::ActionSettings => "SIID_SETTINGS",
723 IconRole::ActionEdit => "Edit",
724 IconRole::ActionAdd => "Add",
725 IconRole::ActionRemove => "Remove",
726 IconRole::ActionRefresh => "Refresh",
727 IconRole::ActionPrint => "SIID_PRINTER",
728
729 IconRole::NavBack => "Back",
731 IconRole::NavForward => "Forward",
732 IconRole::NavUp => "Up",
733 IconRole::NavDown => "Down",
734 IconRole::NavHome => "Home",
735 IconRole::NavMenu => "GlobalNavigationButton",
736
737 IconRole::FileGeneric => "SIID_DOCNOASSOC",
739 IconRole::FolderClosed => "SIID_FOLDER",
740 IconRole::FolderOpen => "SIID_FOLDEROPEN",
741 IconRole::TrashEmpty => "SIID_RECYCLER",
742 IconRole::TrashFull => "SIID_RECYCLERFULL",
743
744 IconRole::StatusBusy => return None,
747 IconRole::StatusCheck => "CheckMark",
748 IconRole::StatusError => "SIID_ERROR",
749
750 IconRole::UserAccount => "SIID_USERS",
752 IconRole::Notification => "Ringer",
753 IconRole::Help => "SIID_HELP",
754 IconRole::Lock => "SIID_LOCK",
755
756 _ => return None,
757 })
758}
759
760#[allow(unreachable_patterns)]
761fn freedesktop_name(role: IconRole) -> Option<&'static str> {
762 Some(match role {
763 IconRole::DialogWarning => "dialog-warning",
765 IconRole::DialogError => "dialog-error",
766 IconRole::DialogInfo => "dialog-information",
767 IconRole::DialogQuestion => "dialog-question",
768 IconRole::DialogSuccess => "emblem-ok-symbolic",
769 IconRole::Shield => "security-high",
770
771 IconRole::WindowClose => "window-close",
773 IconRole::WindowMinimize => "window-minimize",
774 IconRole::WindowMaximize => "window-maximize",
775 IconRole::WindowRestore => "window-restore",
776
777 IconRole::ActionSave => "document-save",
779 IconRole::ActionDelete => "edit-delete",
780 IconRole::ActionCopy => "edit-copy",
781 IconRole::ActionPaste => "edit-paste",
782 IconRole::ActionCut => "edit-cut",
783 IconRole::ActionUndo => "edit-undo",
784 IconRole::ActionRedo => "edit-redo",
785 IconRole::ActionSearch => "edit-find",
786 IconRole::ActionSettings => "preferences-system",
787 IconRole::ActionEdit => "document-edit",
788 IconRole::ActionAdd => "list-add",
789 IconRole::ActionRemove => "list-remove",
790 IconRole::ActionRefresh => "view-refresh",
791 IconRole::ActionPrint => "document-print",
792
793 IconRole::NavBack => "go-previous",
795 IconRole::NavForward => "go-next",
796 IconRole::NavUp => "go-up",
797 IconRole::NavDown => "go-down",
798 IconRole::NavHome => "go-home",
799 IconRole::NavMenu => "open-menu",
800
801 IconRole::FileGeneric => "text-x-generic",
803 IconRole::FolderClosed => "folder",
804 IconRole::FolderOpen => "folder-open",
805 IconRole::TrashEmpty => "user-trash",
806 IconRole::TrashFull => "user-trash-full",
807
808 IconRole::StatusBusy => "process-working",
810 IconRole::StatusCheck => "emblem-default",
811 IconRole::StatusError => "dialog-error",
812
813 IconRole::UserAccount => "system-users",
815 IconRole::Notification => "notification-active",
817 IconRole::Help => "help-browser",
818 IconRole::Lock => "system-lock-screen",
819
820 _ => return None,
821 })
822}
823
824#[allow(unreachable_patterns)]
825fn material_name(role: IconRole) -> Option<&'static str> {
826 Some(match role {
827 IconRole::DialogWarning => "warning",
829 IconRole::DialogError => "error",
830 IconRole::DialogInfo => "info",
831 IconRole::DialogQuestion => "help",
832 IconRole::DialogSuccess => "check_circle",
833 IconRole::Shield => "shield",
834
835 IconRole::WindowClose => "close",
837 IconRole::WindowMinimize => "minimize",
838 IconRole::WindowMaximize => "open_in_full",
839 IconRole::WindowRestore => "close_fullscreen",
840
841 IconRole::ActionSave => "save",
843 IconRole::ActionDelete => "delete",
844 IconRole::ActionCopy => "content_copy",
845 IconRole::ActionPaste => "content_paste",
846 IconRole::ActionCut => "content_cut",
847 IconRole::ActionUndo => "undo",
848 IconRole::ActionRedo => "redo",
849 IconRole::ActionSearch => "search",
850 IconRole::ActionSettings => "settings",
851 IconRole::ActionEdit => "edit",
852 IconRole::ActionAdd => "add",
853 IconRole::ActionRemove => "remove",
854 IconRole::ActionRefresh => "refresh",
855 IconRole::ActionPrint => "print",
856
857 IconRole::NavBack => "arrow_back",
859 IconRole::NavForward => "arrow_forward",
860 IconRole::NavUp => "arrow_upward",
861 IconRole::NavDown => "arrow_downward",
862 IconRole::NavHome => "home",
863 IconRole::NavMenu => "menu",
864
865 IconRole::FileGeneric => "description",
867 IconRole::FolderClosed => "folder",
868 IconRole::FolderOpen => "folder_open",
869 IconRole::TrashEmpty => "delete",
870 IconRole::TrashFull => "delete",
872
873 IconRole::StatusBusy => "progress_activity",
875 IconRole::StatusCheck => "check",
876 IconRole::StatusError => "error",
877
878 IconRole::UserAccount => "person",
880 IconRole::Notification => "notifications",
881 IconRole::Help => "help",
882 IconRole::Lock => "lock",
883
884 _ => return None,
885 })
886}
887
888#[allow(unreachable_patterns)]
889fn lucide_name(role: IconRole) -> Option<&'static str> {
890 Some(match role {
891 IconRole::DialogWarning => "triangle-alert",
893 IconRole::DialogError => "circle-x",
894 IconRole::DialogInfo => "info",
895 IconRole::DialogQuestion => "circle-question-mark",
896 IconRole::DialogSuccess => "circle-check",
897 IconRole::Shield => "shield",
898
899 IconRole::WindowClose => "x",
901 IconRole::WindowMinimize => "minimize",
902 IconRole::WindowMaximize => "maximize",
903 IconRole::WindowRestore => "minimize-2",
904
905 IconRole::ActionSave => "save",
907 IconRole::ActionDelete => "trash-2",
908 IconRole::ActionCopy => "copy",
909 IconRole::ActionPaste => "clipboard-paste",
910 IconRole::ActionCut => "scissors",
911 IconRole::ActionUndo => "undo-2",
912 IconRole::ActionRedo => "redo-2",
913 IconRole::ActionSearch => "search",
914 IconRole::ActionSettings => "settings",
915 IconRole::ActionEdit => "pencil",
916 IconRole::ActionAdd => "plus",
917 IconRole::ActionRemove => "minus",
918 IconRole::ActionRefresh => "refresh-cw",
919 IconRole::ActionPrint => "printer",
920
921 IconRole::NavBack => "chevron-left",
923 IconRole::NavForward => "chevron-right",
924 IconRole::NavUp => "chevron-up",
925 IconRole::NavDown => "chevron-down",
926 IconRole::NavHome => "house",
927 IconRole::NavMenu => "menu",
928
929 IconRole::FileGeneric => "file",
931 IconRole::FolderClosed => "folder-closed",
932 IconRole::FolderOpen => "folder-open",
933 IconRole::TrashEmpty => "trash-2",
934 IconRole::TrashFull => "trash-2",
936
937 IconRole::StatusBusy => "loader",
939 IconRole::StatusCheck => "check",
940 IconRole::StatusError => "circle-x",
941
942 IconRole::UserAccount => "user",
944 IconRole::Notification => "bell",
945 IconRole::Help => "circle-question-mark",
946 IconRole::Lock => "lock",
947
948 _ => return None,
949 })
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
959 fn icon_role_all_has_42_variants() {
960 assert_eq!(IconRole::ALL.len(), 42);
961 }
962
963 #[test]
964 fn icon_role_all_contains_every_variant() {
965 let all = &IconRole::ALL;
967
968 assert!(all.contains(&IconRole::DialogWarning));
970 assert!(all.contains(&IconRole::DialogError));
971 assert!(all.contains(&IconRole::DialogInfo));
972 assert!(all.contains(&IconRole::DialogQuestion));
973 assert!(all.contains(&IconRole::DialogSuccess));
974 assert!(all.contains(&IconRole::Shield));
975
976 assert!(all.contains(&IconRole::WindowClose));
978 assert!(all.contains(&IconRole::WindowMinimize));
979 assert!(all.contains(&IconRole::WindowMaximize));
980 assert!(all.contains(&IconRole::WindowRestore));
981
982 assert!(all.contains(&IconRole::ActionSave));
984 assert!(all.contains(&IconRole::ActionDelete));
985 assert!(all.contains(&IconRole::ActionCopy));
986 assert!(all.contains(&IconRole::ActionPaste));
987 assert!(all.contains(&IconRole::ActionCut));
988 assert!(all.contains(&IconRole::ActionUndo));
989 assert!(all.contains(&IconRole::ActionRedo));
990 assert!(all.contains(&IconRole::ActionSearch));
991 assert!(all.contains(&IconRole::ActionSettings));
992 assert!(all.contains(&IconRole::ActionEdit));
993 assert!(all.contains(&IconRole::ActionAdd));
994 assert!(all.contains(&IconRole::ActionRemove));
995 assert!(all.contains(&IconRole::ActionRefresh));
996 assert!(all.contains(&IconRole::ActionPrint));
997
998 assert!(all.contains(&IconRole::NavBack));
1000 assert!(all.contains(&IconRole::NavForward));
1001 assert!(all.contains(&IconRole::NavUp));
1002 assert!(all.contains(&IconRole::NavDown));
1003 assert!(all.contains(&IconRole::NavHome));
1004 assert!(all.contains(&IconRole::NavMenu));
1005
1006 assert!(all.contains(&IconRole::FileGeneric));
1008 assert!(all.contains(&IconRole::FolderClosed));
1009 assert!(all.contains(&IconRole::FolderOpen));
1010 assert!(all.contains(&IconRole::TrashEmpty));
1011 assert!(all.contains(&IconRole::TrashFull));
1012
1013 assert!(all.contains(&IconRole::StatusBusy));
1015 assert!(all.contains(&IconRole::StatusCheck));
1016 assert!(all.contains(&IconRole::StatusError));
1017
1018 assert!(all.contains(&IconRole::UserAccount));
1020 assert!(all.contains(&IconRole::Notification));
1021 assert!(all.contains(&IconRole::Help));
1022 assert!(all.contains(&IconRole::Lock));
1023 }
1024
1025 #[test]
1026 fn icon_role_all_no_duplicates() {
1027 let all = &IconRole::ALL;
1028 for (i, role) in all.iter().enumerate() {
1029 for (j, other) in all.iter().enumerate() {
1030 if i != j {
1031 assert_ne!(role, other, "Duplicate at index {i} and {j}");
1032 }
1033 }
1034 }
1035 }
1036
1037 #[test]
1038 fn icon_role_derives_copy_clone() {
1039 let role = IconRole::ActionCopy;
1040 let copied1 = role;
1041 let copied2 = role;
1042 assert_eq!(role, copied1);
1043 assert_eq!(role, copied2);
1044 }
1045
1046 #[test]
1047 fn icon_role_derives_debug() {
1048 let s = format!("{:?}", IconRole::DialogWarning);
1049 assert!(s.contains("DialogWarning"));
1050 }
1051
1052 #[test]
1053 fn icon_role_derives_hash() {
1054 use std::collections::HashSet;
1055 let mut set = HashSet::new();
1056 set.insert(IconRole::ActionSave);
1057 set.insert(IconRole::ActionDelete);
1058 assert_eq!(set.len(), 2);
1059 assert!(set.contains(&IconRole::ActionSave));
1060 }
1061
1062 #[test]
1065 fn icon_data_svg_construct_and_match() {
1066 let svg_bytes = b"<svg></svg>".to_vec();
1067 let data = IconData::Svg(svg_bytes.clone());
1068 match data {
1069 IconData::Svg(bytes) => assert_eq!(bytes, svg_bytes),
1070 _ => panic!("Expected Svg variant"),
1071 }
1072 }
1073
1074 #[test]
1075 fn icon_data_rgba_construct_and_match() {
1076 let pixels = vec![255, 0, 0, 255]; let data = IconData::Rgba {
1078 width: 1,
1079 height: 1,
1080 data: pixels.clone(),
1081 };
1082 match data {
1083 IconData::Rgba {
1084 width,
1085 height,
1086 data,
1087 } => {
1088 assert_eq!(width, 1);
1089 assert_eq!(height, 1);
1090 assert_eq!(data, pixels);
1091 }
1092 _ => panic!("Expected Rgba variant"),
1093 }
1094 }
1095
1096 #[test]
1097 fn icon_data_derives_debug() {
1098 let data = IconData::Svg(vec![]);
1099 let s = format!("{:?}", data);
1100 assert!(s.contains("Svg"));
1101 }
1102
1103 #[test]
1104 fn icon_data_derives_clone() {
1105 let data = IconData::Rgba {
1106 width: 16,
1107 height: 16,
1108 data: vec![0; 16 * 16 * 4],
1109 };
1110 let cloned = data.clone();
1111 assert_eq!(data, cloned);
1112 }
1113
1114 #[test]
1115 fn icon_data_derives_eq() {
1116 let a = IconData::Svg(b"<svg/>".to_vec());
1117 let b = IconData::Svg(b"<svg/>".to_vec());
1118 assert_eq!(a, b);
1119
1120 let c = IconData::Svg(b"<other/>".to_vec());
1121 assert_ne!(a, c);
1122 }
1123
1124 #[test]
1127 fn icon_set_from_name_sf_symbols() {
1128 assert_eq!(IconSet::from_name("sf-symbols"), Some(IconSet::SfSymbols));
1129 }
1130
1131 #[test]
1132 fn icon_set_from_name_segoe_fluent() {
1133 assert_eq!(
1134 IconSet::from_name("segoe-fluent"),
1135 Some(IconSet::SegoeIcons)
1136 );
1137 }
1138
1139 #[test]
1140 fn icon_set_from_name_freedesktop() {
1141 assert_eq!(
1142 IconSet::from_name("freedesktop"),
1143 Some(IconSet::Freedesktop)
1144 );
1145 }
1146
1147 #[test]
1148 fn icon_set_from_name_material() {
1149 assert_eq!(IconSet::from_name("material"), Some(IconSet::Material));
1150 }
1151
1152 #[test]
1153 fn icon_set_from_name_lucide() {
1154 assert_eq!(IconSet::from_name("lucide"), Some(IconSet::Lucide));
1155 }
1156
1157 #[test]
1158 fn icon_set_from_name_unknown() {
1159 assert_eq!(IconSet::from_name("unknown"), None);
1160 }
1161
1162 #[test]
1163 fn icon_set_name_sf_symbols() {
1164 assert_eq!(IconSet::SfSymbols.name(), "sf-symbols");
1165 }
1166
1167 #[test]
1168 fn icon_set_name_segoe_fluent() {
1169 assert_eq!(IconSet::SegoeIcons.name(), "segoe-fluent");
1170 }
1171
1172 #[test]
1173 fn icon_set_name_freedesktop() {
1174 assert_eq!(IconSet::Freedesktop.name(), "freedesktop");
1175 }
1176
1177 #[test]
1178 fn icon_set_name_material() {
1179 assert_eq!(IconSet::Material.name(), "material");
1180 }
1181
1182 #[test]
1183 fn icon_set_name_lucide() {
1184 assert_eq!(IconSet::Lucide.name(), "lucide");
1185 }
1186
1187 #[test]
1188 fn icon_set_from_name_name_round_trip() {
1189 let sets = [
1190 IconSet::SfSymbols,
1191 IconSet::SegoeIcons,
1192 IconSet::Freedesktop,
1193 IconSet::Material,
1194 IconSet::Lucide,
1195 ];
1196 for set in &sets {
1197 let name = set.name();
1198 let parsed = IconSet::from_name(name);
1199 assert_eq!(parsed, Some(*set), "Round-trip failed for {:?}", set);
1200 }
1201 }
1202
1203 #[test]
1204 fn icon_set_derives_copy_clone() {
1205 let set = IconSet::Material;
1206 let copied1 = set;
1207 let copied2 = set;
1208 assert_eq!(set, copied1);
1209 assert_eq!(set, copied2);
1210 }
1211
1212 #[test]
1213 fn icon_set_derives_hash() {
1214 use std::collections::HashSet;
1215 let mut map = HashSet::new();
1216 map.insert(IconSet::SfSymbols);
1217 map.insert(IconSet::Lucide);
1218 assert_eq!(map.len(), 2);
1219 }
1220
1221 #[test]
1222 fn icon_set_derives_debug() {
1223 let s = format!("{:?}", IconSet::Freedesktop);
1224 assert!(s.contains("Freedesktop"));
1225 }
1226
1227 #[test]
1228 fn icon_set_serde_round_trip() {
1229 let set = IconSet::SfSymbols;
1230 let json = serde_json::to_string(&set).unwrap();
1231 let deserialized: IconSet = serde_json::from_str(&json).unwrap();
1232 assert_eq!(set, deserialized);
1233 }
1234
1235 #[test]
1238 fn icon_name_sf_symbols_action_copy() {
1239 assert_eq!(
1240 icon_name(IconSet::SfSymbols, IconRole::ActionCopy),
1241 Some("doc.on.doc")
1242 );
1243 }
1244
1245 #[test]
1246 fn icon_name_segoe_action_copy() {
1247 assert_eq!(
1248 icon_name(IconSet::SegoeIcons, IconRole::ActionCopy),
1249 Some("Copy")
1250 );
1251 }
1252
1253 #[test]
1254 fn icon_name_freedesktop_action_copy() {
1255 assert_eq!(
1256 icon_name(IconSet::Freedesktop, IconRole::ActionCopy),
1257 Some("edit-copy")
1258 );
1259 }
1260
1261 #[test]
1262 fn icon_name_material_action_copy() {
1263 assert_eq!(
1264 icon_name(IconSet::Material, IconRole::ActionCopy),
1265 Some("content_copy")
1266 );
1267 }
1268
1269 #[test]
1270 fn icon_name_lucide_action_copy() {
1271 assert_eq!(
1272 icon_name(IconSet::Lucide, IconRole::ActionCopy),
1273 Some("copy")
1274 );
1275 }
1276
1277 #[test]
1278 fn icon_name_sf_symbols_dialog_warning() {
1279 assert_eq!(
1280 icon_name(IconSet::SfSymbols, IconRole::DialogWarning),
1281 Some("exclamationmark.triangle.fill")
1282 );
1283 }
1284
1285 #[test]
1287 fn icon_name_sf_symbols_folder_open_is_none() {
1288 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::FolderOpen), None);
1289 }
1290
1291 #[test]
1292 fn icon_name_sf_symbols_trash_full() {
1293 assert_eq!(
1294 icon_name(IconSet::SfSymbols, IconRole::TrashFull),
1295 Some("trash.fill")
1296 );
1297 }
1298
1299 #[test]
1300 fn icon_name_sf_symbols_status_busy_is_none() {
1301 assert_eq!(icon_name(IconSet::SfSymbols, IconRole::StatusBusy), None);
1302 }
1303
1304 #[test]
1305 fn icon_name_sf_symbols_window_restore() {
1306 assert_eq!(
1307 icon_name(IconSet::SfSymbols, IconRole::WindowRestore),
1308 Some("arrow.down.right.and.arrow.up.left")
1309 );
1310 }
1311
1312 #[test]
1313 fn icon_name_segoe_dialog_success() {
1314 assert_eq!(
1315 icon_name(IconSet::SegoeIcons, IconRole::DialogSuccess),
1316 Some("CheckMark")
1317 );
1318 }
1319
1320 #[test]
1321 fn icon_name_segoe_status_busy_is_none() {
1322 assert_eq!(icon_name(IconSet::SegoeIcons, IconRole::StatusBusy), None);
1323 }
1324
1325 #[test]
1326 fn icon_name_freedesktop_notification() {
1327 assert_eq!(
1328 icon_name(IconSet::Freedesktop, IconRole::Notification),
1329 Some("notification-active")
1330 );
1331 }
1332
1333 #[test]
1334 fn icon_name_material_trash_full() {
1335 assert_eq!(
1336 icon_name(IconSet::Material, IconRole::TrashFull),
1337 Some("delete")
1338 );
1339 }
1340
1341 #[test]
1342 fn icon_name_lucide_trash_full() {
1343 assert_eq!(
1344 icon_name(IconSet::Lucide, IconRole::TrashFull),
1345 Some("trash-2")
1346 );
1347 }
1348
1349 #[test]
1351 fn icon_name_spot_check_dialog_error() {
1352 assert_eq!(
1353 icon_name(IconSet::SfSymbols, IconRole::DialogError),
1354 Some("xmark.circle.fill")
1355 );
1356 assert_eq!(
1357 icon_name(IconSet::SegoeIcons, IconRole::DialogError),
1358 Some("SIID_ERROR")
1359 );
1360 assert_eq!(
1361 icon_name(IconSet::Freedesktop, IconRole::DialogError),
1362 Some("dialog-error")
1363 );
1364 assert_eq!(
1365 icon_name(IconSet::Material, IconRole::DialogError),
1366 Some("error")
1367 );
1368 assert_eq!(
1369 icon_name(IconSet::Lucide, IconRole::DialogError),
1370 Some("circle-x")
1371 );
1372 }
1373
1374 #[test]
1375 fn icon_name_spot_check_nav_home() {
1376 assert_eq!(
1377 icon_name(IconSet::SfSymbols, IconRole::NavHome),
1378 Some("house")
1379 );
1380 assert_eq!(
1381 icon_name(IconSet::SegoeIcons, IconRole::NavHome),
1382 Some("Home")
1383 );
1384 assert_eq!(
1385 icon_name(IconSet::Freedesktop, IconRole::NavHome),
1386 Some("go-home")
1387 );
1388 assert_eq!(
1389 icon_name(IconSet::Material, IconRole::NavHome),
1390 Some("home")
1391 );
1392 assert_eq!(icon_name(IconSet::Lucide, IconRole::NavHome), Some("house"));
1393 }
1394
1395 #[test]
1397 fn icon_name_sf_symbols_expected_count() {
1398 let some_count = IconRole::ALL
1400 .iter()
1401 .filter(|r| icon_name(IconSet::SfSymbols, **r).is_some())
1402 .count();
1403 assert_eq!(some_count, 40, "SF Symbols should have 40 mappings");
1404 }
1405
1406 #[test]
1407 fn icon_name_segoe_expected_count() {
1408 let some_count = IconRole::ALL
1410 .iter()
1411 .filter(|r| icon_name(IconSet::SegoeIcons, **r).is_some())
1412 .count();
1413 assert_eq!(some_count, 41, "Segoe Icons should have 41 mappings");
1414 }
1415
1416 #[test]
1417 fn icon_name_freedesktop_expected_count() {
1418 let some_count = IconRole::ALL
1420 .iter()
1421 .filter(|r| icon_name(IconSet::Freedesktop, **r).is_some())
1422 .count();
1423 assert_eq!(some_count, 42, "Freedesktop should have 42 mappings");
1424 }
1425
1426 #[test]
1427 fn icon_name_material_expected_count() {
1428 let some_count = IconRole::ALL
1430 .iter()
1431 .filter(|r| icon_name(IconSet::Material, **r).is_some())
1432 .count();
1433 assert_eq!(some_count, 42, "Material should have 42 mappings");
1434 }
1435
1436 #[test]
1437 fn icon_name_lucide_expected_count() {
1438 let some_count = IconRole::ALL
1440 .iter()
1441 .filter(|r| icon_name(IconSet::Lucide, **r).is_some())
1442 .count();
1443 assert_eq!(some_count, 42, "Lucide should have 42 mappings");
1444 }
1445
1446 #[test]
1449 #[cfg(target_os = "linux")]
1450 fn system_icon_set_returns_freedesktop_on_linux() {
1451 assert_eq!(system_icon_set(), IconSet::Freedesktop);
1452 }
1453
1454 #[test]
1455 fn system_icon_theme_returns_non_empty() {
1456 let theme = system_icon_theme();
1457 assert!(
1458 !theme.is_empty(),
1459 "system_icon_theme() should return a non-empty string"
1460 );
1461 }
1462
1463 #[test]
1466 fn icon_provider_is_object_safe() {
1467 let provider: Box<dyn IconProvider> = Box::new(IconRole::ActionCopy);
1469 let debug_str = format!("{:?}", provider);
1470 assert!(
1471 debug_str.contains("ActionCopy"),
1472 "Debug should print variant name"
1473 );
1474 }
1475
1476 #[test]
1477 fn icon_role_provider_icon_name() {
1478 let role = IconRole::ActionCopy;
1480 let name = IconProvider::icon_name(&role, IconSet::Material);
1481 assert_eq!(name, Some("content_copy"));
1482 }
1483
1484 #[test]
1485 fn icon_role_provider_icon_name_sf_symbols() {
1486 let role = IconRole::ActionCopy;
1487 let name = IconProvider::icon_name(&role, IconSet::SfSymbols);
1488 assert_eq!(name, Some("doc.on.doc"));
1489 }
1490
1491 #[test]
1492 #[cfg(feature = "material-icons")]
1493 fn icon_role_provider_icon_svg_material() {
1494 let role = IconRole::ActionCopy;
1495 let svg = IconProvider::icon_svg(&role, IconSet::Material);
1496 assert!(svg.is_some(), "Material SVG should be Some");
1497 let content = std::str::from_utf8(svg.unwrap()).expect("valid UTF-8");
1498 assert!(content.contains("<svg"), "should contain <svg tag");
1499 }
1500
1501 #[test]
1502 fn icon_role_provider_icon_svg_non_bundled() {
1503 let role = IconRole::ActionCopy;
1505 let svg = IconProvider::icon_svg(&role, IconSet::SfSymbols);
1506 assert!(svg.is_none(), "SfSymbols should not have bundled SVGs");
1507 }
1508
1509 #[test]
1510 fn icon_role_provider_all_roles() {
1511 for role in IconRole::ALL {
1513 let _name = IconProvider::icon_name(&role, IconSet::Material);
1515 }
1517 }
1518
1519 #[test]
1520 fn icon_provider_dyn_dispatch() {
1521 let role = IconRole::ActionCopy;
1523 let provider: &dyn IconProvider = &role;
1524 let name = provider.icon_name(IconSet::Material);
1525 assert_eq!(name, Some("content_copy"));
1526 let svg = provider.icon_svg(IconSet::SfSymbols);
1527 assert!(svg.is_none(), "SfSymbols should not have bundled SVGs");
1528 }
1529
1530 fn known_gaps() -> &'static [(IconSet, IconRole)] {
1533 &[
1534 (IconSet::SfSymbols, IconRole::FolderOpen),
1535 (IconSet::SfSymbols, IconRole::StatusBusy),
1536 (IconSet::SegoeIcons, IconRole::StatusBusy),
1537 ]
1538 }
1539
1540 #[test]
1541 fn no_unexpected_icon_gaps() {
1542 let gaps = known_gaps();
1543 let system_sets = [
1544 IconSet::SfSymbols,
1545 IconSet::SegoeIcons,
1546 IconSet::Freedesktop,
1547 ];
1548 for &set in &system_sets {
1549 for role in IconRole::ALL {
1550 let is_known_gap = gaps.contains(&(set, role));
1551 let is_mapped = icon_name(set, role).is_some();
1552 if !is_known_gap {
1553 assert!(
1554 is_mapped,
1555 "{role:?} has no mapping for {set:?} and is not in known_gaps()"
1556 );
1557 }
1558 }
1559 }
1560 }
1561
1562 #[test]
1563 #[cfg(all(feature = "material-icons", feature = "lucide-icons"))]
1564 fn all_roles_have_bundled_svg() {
1565 use crate::bundled_icon_svg;
1566 for set in [IconSet::Material, IconSet::Lucide] {
1567 for role in IconRole::ALL {
1568 assert!(
1569 bundled_icon_svg(set, role).is_some(),
1570 "{role:?} has no bundled SVG for {set:?}"
1571 );
1572 }
1573 }
1574 }
1575}