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