Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/windowing/winit/linux/cursor_theme.rs
1use std::{env, path::PathBuf};
2use tini::Ini;
3 
4static CURSOR_DIR_NAME: &'static &str = &"cursors";
5static CURSOR_INDEX_FILE_NAME: &'static &str = &"index.theme";
6static THEME_FILE_CURSOR_SECTION: &'static &str = &"Icon Theme";
7static THEME_FILE_INHERITS_KEY: &'static &str = &"Inherits";
8 
9static ENV_DATA_DIRS: &'static &str = &"XDG_DATA_DIRS";
10static ENV_CURSOR_THEME: &'static &str = &"XCURSOR_THEME";
11 
12static DEFAULT_THEME: &'static &str = &"default";
13static KNOWN_THEMES: &[&str] = &["Yaru", "Adwaita"];
14 
15pub fn ensure_cursor_theme() {
16 // If the XCURSOR_THEME value is explicitly set,
17 // then we do not want to modify the user's environment
18 if env::var(ENV_CURSOR_THEME).is_ok() {
19 return;
20 }
21 
22 let crawler = CursorThemeCrawler::new();
23 
24 if let Some(theme) = crawler.determine_cursor_theme() {
25 // winit and it's dependencies will automatically check for
26 // the default theme, so we do not need to mess with the
27 // env var here.
28 if theme != *DEFAULT_THEME {
29 env::set_var(ENV_CURSOR_THEME, theme);
30 }
31 }
32}
33 
34struct CursorThemeCrawler {
35 /// Directories to search when looking for a cursor theme.
36 /// Directories are searched in vec order from first to last.
37 /// However, because themes can reference other themes, it is
38 /// possible for search results to traverse multiple directories.
39 /// For example, a default theme can be found in directories[1]
40 /// that inherits from a theme in directories[3], which itself
41 /// inherits from a theme in directories[0]
42 directories: Vec<PathBuf>,
43}
44 
45fn non_empty_var(name: &str) -> Option<String> {
46 env::var(name).ok().filter(|val| !val.is_empty())
47}
48 
49impl CursorThemeCrawler {
50 pub fn new() -> Self {
51 // Per https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout,
52 // we search:
53 // - $HOME/.icons (for backwards compatibility)
54 // - $XDG_DATA_HOME/icons (technically this should be part of XDG_DATA_DIRS, but we add it in here)
55 // - Defaults to $HOME/.local/share
56 // - $XDG_DATA_DIRS/icons
57 // - Defaults to /usr/local/share/:/usr/share/
58 // - /usr/share/pixmaps
59 let mut directories = vec![];
60 
61 let xdg_data_dirs = non_empty_var(ENV_DATA_DIRS)
62 .or_else(|| Some("/usr/local/share/:/usr/share/".to_string()));
63 
64 if let Some(home) = dirs::home_dir() {
65 directories.push(home.join(".icons"));
66 }
67 if let Some(xdg_data_home) = dirs::data_dir() {
68 directories.push(xdg_data_home.join("icons"));
69 }
70 if let Some(xdg_data_dirs) = xdg_data_dirs {
71 for dir in xdg_data_dirs.split(':') {
72 if !dir.is_empty() {
73 directories.push(PathBuf::from(dir).join("icons"));
74 }
75 }
76 }
77 directories.push(PathBuf::from("/usr/share/pixmaps"));
78 Self { directories }
79 }
80 
81 /// First checks to see if there is a default cursor theme set.
82 /// If there is no default set, we check a list of known themes.
83 /// The first theme to be confirmed exist is returned, else None
84 /// is returned.
85 fn determine_cursor_theme(&self) -> Option<String> {
86 if self.check_cursor_theme(DEFAULT_THEME) {
87 return Some(DEFAULT_THEME.to_string());
88 }
89 
90 for theme in KNOWN_THEMES {
91 if self.check_cursor_theme(theme) {
92 return Some((*theme).to_string());
93 }
94 }
95 None
96 }
97 
98 /// Returns true if an icon theme exists and has a `cursors/`
99 /// folder, indicating that the cursors for that theme are installed.
100 /// Per the specification, an icon theme can exist along multiple
101 /// directories. As long as at least one of those directories
102 /// contains the `cursors/` subdir, we consider it valid
103 fn check_cursor_theme_installed(&self, theme: &str) -> bool {
104 for dir in &self.directories {
105 if dir.join(theme).join(CURSOR_DIR_NAME).exists() {
106 return true;
107 }
108 }
109 false
110 }
111 
112 /// Checks that a given icon theme is a valid cursor theme.
113 ///
114 /// When we check a cursor theme, we verify that either:
115 /// a. The icon theme has a cursors/ folder
116 /// b. The icon theme inherits from an existing cursor theme.
117 ///
118 /// This can cause us to traverse multiple themes as part of our validation.
119 /// we do this verification to handle cases like the `adwaita-icon-theme`
120 /// deb packages, which sets Adwaita to the default icon theme without
121 /// installing a cursor theme.
122 fn check_cursor_theme(&self, root_theme: &str) -> bool {
123 let mut visited = std::collections::HashSet::from([root_theme.to_string()]);
124 let mut pending = std::collections::VecDeque::from([root_theme.to_string()]);
125 
126 while let Some(theme) = pending.pop_front() {
127 if self.check_cursor_theme_installed(&theme) {
128 return true;
129 }
130 
131 // Per the spec, the **first** index.theme found when traversing
132 // the directories is used
133 let inherited_themes = &self
134 .directories
135 .iter()
136 .filter_map(|index_dir| {
137 let index_path = index_dir.join(&theme).join(CURSOR_INDEX_FILE_NAME);
138 if let Ok(theme_file) = Ini::from_file(&index_path) {
139 theme_file.get_vec_with_sep::<String>(
140 THEME_FILE_CURSOR_SECTION,
141 THEME_FILE_INHERITS_KEY,
142 ",",
143 )
144 } else {
145 None
146 }
147 })
148 .next();
149 
150 if let Some(inherited_themes) = inherited_themes {
151 for new_theme in inherited_themes {
152 if !visited.contains(new_theme) {
153 visited.insert(new_theme.clone());
154 pending.push_back(new_theme.clone());
155 }
156 }
157 }
158 }
159 
160 false
161 }
162}
163 
164#[cfg(test)]
165#[path = "cursor_theme_tests.rs"]
166mod tests;
167