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