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