perf: optimizations and additional test cases
This commit is contained in:
parent
db4d26e591
commit
44edef9673
9 changed files with 225 additions and 226 deletions
|
|
@ -28,29 +28,29 @@ impl Directory<'_> {
|
|||
}
|
||||
|
||||
pub fn directory_size_distance(&self, size: u16, scale: u16) -> i16 {
|
||||
let scaled_size = self.size * self.scale;
|
||||
let min_scaled_size = self.minsize * self.scale;
|
||||
let max_scaled_size = self.maxsize * self.scale;
|
||||
let scale = scale as i16;
|
||||
let size = size as i16;
|
||||
let scaled_requested_size = size * scale;
|
||||
let scaled_requested_size = size as i16 * scale;
|
||||
|
||||
match self.type_ {
|
||||
DirectoryType::Fixed => scaled_size - scaled_requested_size,
|
||||
DirectoryType::Fixed => self.size * self.scale - scaled_requested_size,
|
||||
DirectoryType::Scalable => {
|
||||
let min_scaled_size = self.minsize * self.scale;
|
||||
if scaled_requested_size < min_scaled_size {
|
||||
min_scaled_size - scaled_requested_size
|
||||
} else if scaled_requested_size < max_scaled_size {
|
||||
scaled_requested_size - max_scaled_size
|
||||
} else {
|
||||
0
|
||||
let max_scaled_size = self.maxsize * self.scale;
|
||||
if scaled_requested_size < max_scaled_size {
|
||||
scaled_requested_size - max_scaled_size
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
DirectoryType::Threshold => {
|
||||
if scaled_requested_size < (self.size - self.threshold) * scale {
|
||||
min_scaled_size - scaled_requested_size
|
||||
self.minsize * self.scale - scaled_requested_size
|
||||
} else if scaled_requested_size > (self.size + self.threshold) * scale {
|
||||
scaled_requested_size - max_scaled_size
|
||||
scaled_requested_size - self.maxsize * self.scale
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
@ -74,9 +74,9 @@ impl Default for DirectoryType {
|
|||
|
||||
impl From<&str> for DirectoryType {
|
||||
fn from(value: &str) -> Self {
|
||||
match value {
|
||||
"Fixed" => DirectoryType::Fixed,
|
||||
"Scalable" => DirectoryType::Scalable,
|
||||
match value.as_bytes()[0] {
|
||||
b'F' => DirectoryType::Fixed,
|
||||
b'S' => DirectoryType::Scalable,
|
||||
_ => DirectoryType::Threshold,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum ThemeError {
|
||||
#[error("No 'index.theme' file for {0}")]
|
||||
ThemeIndexNotFound(PathBuf),
|
||||
#[error("IoError: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
}
|
||||
161
src/theme/mod.rs
161
src/theme/mod.rs
|
|
@ -1,18 +1,15 @@
|
|||
use crate::theme::error::ThemeError;
|
||||
use crate::theme::paths::ThemePath;
|
||||
use memmap2::Mmap;
|
||||
pub(crate) use paths::BASE_PATHS;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::ControlFlow;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
mod directories;
|
||||
pub mod error;
|
||||
mod parse;
|
||||
mod paths;
|
||||
|
||||
type Result<T> = std::result::Result<T, ThemeError>;
|
||||
|
||||
pub static THEMES: LazyLock<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
|
||||
|
||||
#[inline]
|
||||
|
|
@ -50,8 +47,7 @@ impl Theme {
|
|||
scale: u16,
|
||||
force_svg: bool,
|
||||
) -> Option<PathBuf> {
|
||||
self.match_size(file, size, scale)
|
||||
.find_map(|path| try_build_icon_path(name, path, force_svg))
|
||||
self.try_fold_icon_path(self.match_size(file, size, scale), name, force_svg)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -60,12 +56,10 @@ impl Theme {
|
|||
file: &'a str,
|
||||
size: u16,
|
||||
scale: u16,
|
||||
) -> impl Iterator<Item = PathBuf> + 'a {
|
||||
let dirs = self.get_all_directories(file);
|
||||
|
||||
dirs.filter(move |directory| directory.match_size(size, scale))
|
||||
) -> impl Iterator<Item = &'a str> + 'a {
|
||||
self.get_all_directories(file)
|
||||
.filter(move |directory| directory.match_size(size, scale))
|
||||
.map(|dir| dir.name)
|
||||
.map(|dir| self.path().join(dir))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -77,32 +71,59 @@ impl Theme {
|
|||
scale: u16,
|
||||
force_svg: bool,
|
||||
) -> Option<PathBuf> {
|
||||
self.closest_match_size(file, size, scale)
|
||||
.iter()
|
||||
.find_map(|path| try_build_icon_path(name, path, force_svg))
|
||||
self.try_fold_icon_path(self.closest_match_size(file, size, scale), name, force_svg)
|
||||
}
|
||||
|
||||
fn closest_match_size(&self, file: &str, size: u16, scale: u16) -> Vec<PathBuf> {
|
||||
let dirs = self.get_all_directories(file);
|
||||
|
||||
let mut dirs: Vec<_> = dirs
|
||||
.filter_map(|directory| {
|
||||
let distance = directory.directory_size_distance(size, scale);
|
||||
if distance < i16::MAX {
|
||||
Some((directory, distance.abs()))
|
||||
fn try_fold_icon_path<'a>(
|
||||
&self,
|
||||
mut dir_names: impl Iterator<Item = &'a str>,
|
||||
name: &str,
|
||||
force_svg: bool,
|
||||
) -> Option<PathBuf> {
|
||||
dir_names
|
||||
.try_fold(self.path().clone(), move |mut path, dir_name| {
|
||||
path.push(dir_name);
|
||||
if try_build_icon_path(&mut path, name, force_svg) {
|
||||
ControlFlow::Break(path)
|
||||
} else {
|
||||
None
|
||||
let components = dir_name
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.fold(2, |n, c| n + (*c == b'/') as u32)
|
||||
as usize;
|
||||
|
||||
for _ in 0..components {
|
||||
path.pop();
|
||||
}
|
||||
|
||||
ControlFlow::Continue(path)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.break_value()
|
||||
}
|
||||
|
||||
dirs.sort_by(|(_, a), (_, b)| a.cmp(b));
|
||||
fn closest_match_size<'a>(
|
||||
&'a self,
|
||||
file: &'a str,
|
||||
size: u16,
|
||||
scale: u16,
|
||||
) -> impl Iterator<Item = &'a str> + 'a {
|
||||
let dirs = self.get_all_directories(file);
|
||||
|
||||
dirs.iter()
|
||||
.map(|(dir, _)| dir)
|
||||
.map(|dir| dir.name)
|
||||
.map(|dir| self.path().join(dir))
|
||||
.collect()
|
||||
dirs.fold(Vec::<(&'a str, i16)>::new(), |mut sorted, directory| {
|
||||
let distance = directory.directory_size_distance(size, scale);
|
||||
if distance < i16::MAX {
|
||||
let a = distance.abs();
|
||||
let pos = sorted
|
||||
.binary_search_by(|(_, b)| b.cmp(&a))
|
||||
.unwrap_or_else(|pos| pos);
|
||||
sorted.insert(pos, (directory.name, a));
|
||||
}
|
||||
|
||||
sorted
|
||||
})
|
||||
.into_iter()
|
||||
.map(|(name, _)| name)
|
||||
}
|
||||
|
||||
fn path(&self) -> &PathBuf {
|
||||
|
|
@ -110,40 +131,27 @@ impl Theme {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn try_build_icon_path<P: AsRef<Path>>(
|
||||
name: &str,
|
||||
path: P,
|
||||
force_svg: bool,
|
||||
) -> Option<PathBuf> {
|
||||
pub(super) fn try_build_icon_path<'a>(path: &'a mut PathBuf, name: &str, force_svg: bool) -> bool {
|
||||
let mut name_buf = String::with_capacity(name.len() + 4);
|
||||
name_buf.push_str(name);
|
||||
path.push(name);
|
||||
if force_svg {
|
||||
try_build_svg(name, path.as_ref())
|
||||
.or_else(|| try_build_png(name, path.as_ref()))
|
||||
.or_else(|| try_build_xmp(name, path.as_ref()))
|
||||
try_build_ext(path, &mut name_buf, name, ".svg")
|
||||
|| try_build_ext(path, &mut name_buf, name, ".png")
|
||||
|| try_build_ext(path, &mut name_buf, name, ".xmp")
|
||||
} else {
|
||||
try_build_png(name, path.as_ref())
|
||||
.or_else(|| try_build_svg(name, path.as_ref()))
|
||||
.or_else(|| try_build_xmp(name, path.as_ref()))
|
||||
try_build_ext(path, &mut name_buf, name, ".png")
|
||||
|| try_build_ext(path, &mut name_buf, name, ".svg")
|
||||
|| try_build_ext(path, &mut name_buf, name, ".xmp")
|
||||
}
|
||||
}
|
||||
|
||||
fn try_build_svg<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
let svg = path.join(format!("{name}.svg"));
|
||||
|
||||
if svg.exists() { Some(svg) } else { None }
|
||||
}
|
||||
|
||||
fn try_build_png<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
let png = path.join(format!("{name}.png"));
|
||||
|
||||
if png.exists() { Some(png) } else { None }
|
||||
}
|
||||
|
||||
fn try_build_xmp<P: AsRef<Path>>(name: &str, path: P) -> Option<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
let xmp = path.join(format!("{name}.xmp"));
|
||||
if xmp.exists() { Some(xmp) } else { None }
|
||||
#[inline]
|
||||
fn try_build_ext(path: &mut PathBuf, name_buf: &mut String, name: &str, ext: &'static str) -> bool {
|
||||
name_buf.truncate(name.len());
|
||||
name_buf.push_str(ext);
|
||||
path.set_file_name(&name_buf);
|
||||
path.exists()
|
||||
}
|
||||
|
||||
// Iter through the base paths and get all theme directories
|
||||
|
|
@ -180,8 +188,9 @@ pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
|
|||
let name = entry.file_name();
|
||||
let fallback_index = found_indices.get(&name);
|
||||
if let Some(theme) = Theme::from_path(entry.path(), fallback_index) {
|
||||
let name = name.to_string_lossy().to_string();
|
||||
icon_themes.entry(name).or_default().push(theme);
|
||||
if let Some(name) = name.to_str() {
|
||||
icon_themes.entry(name.to_owned()).or_default().push(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,24 +199,26 @@ pub(super) fn get_all_themes() -> BTreeMap<String, Vec<Theme>> {
|
|||
|
||||
impl Theme {
|
||||
pub(crate) fn from_path<P: AsRef<Path>>(path: P, index: Option<&PathBuf>) -> Option<Self> {
|
||||
let path = path.as_ref();
|
||||
let mut path = path.as_ref().to_path_buf();
|
||||
let is_dir = path.is_dir();
|
||||
path.push("index.theme");
|
||||
let local_index_exists = path.exists();
|
||||
let has_index = local_index_exists || index.is_some();
|
||||
|
||||
let has_index = path.join("index.theme").exists() || index.is_some();
|
||||
|
||||
if !has_index || !path.is_dir() {
|
||||
if !has_index || !is_dir {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = ThemePath(path.into());
|
||||
|
||||
match (index, path.index()) {
|
||||
(Some(index), _) => Some(Theme {
|
||||
path,
|
||||
index: index.clone(),
|
||||
}),
|
||||
(None, Ok(index)) => Some(Theme { path, index }),
|
||||
_ => None,
|
||||
}
|
||||
index
|
||||
.cloned()
|
||||
.or_else(|| local_index_exists.then_some(path.clone()))
|
||||
.map(|index| Theme {
|
||||
path: ThemePath({
|
||||
path.pop();
|
||||
path
|
||||
}),
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,27 +105,26 @@ impl Theme {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn inherits<'a>(&self, file: &'a str) -> Vec<&'a str> {
|
||||
pub fn inherits<'a>(&self, file: &'a str) -> impl Iterator<Item = &'a str> {
|
||||
icon_theme_section(file)
|
||||
.find(|&(key, _)| key == "Inherits")
|
||||
.map(|(_, parents)| {
|
||||
.into_iter()
|
||||
.flat_map(|(_, parents)| {
|
||||
parents
|
||||
.split(',')
|
||||
// Filtering out 'hicolor' since we are going to fallback there anyway
|
||||
.filter(|parent| parent != &"hicolor")
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "local_tests")]
|
||||
mod test {
|
||||
use crate::THEMES;
|
||||
use speculoos::prelude::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "local_tests")]
|
||||
fn should_get_theme_parents() {
|
||||
for theme in THEMES.get("Arc").unwrap() {
|
||||
let file = crate::theme::read_ini_theme(&theme.index).ok().unwrap();
|
||||
|
|
|
|||
|
|
@ -3,48 +3,35 @@ use std::path::PathBuf;
|
|||
use std::sync::LazyLock;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::theme;
|
||||
use crate::theme::error::ThemeError;
|
||||
|
||||
pub(crate) static BASE_PATHS: LazyLock<Vec<PathBuf>> = LazyLock::new(icon_theme_base_paths);
|
||||
|
||||
/// Look in $HOME/.icons (for backwards compatibility), in $XDG_DATA_DIRS/icons, in $XDG_DATA_DIRS/pixmaps and in /usr/share/pixmaps (in that order).
|
||||
/// Paths that are not found are filtered out.
|
||||
fn icon_theme_base_paths() -> Vec<PathBuf> {
|
||||
let base_dirs = BaseDirectories::new();
|
||||
let mut data_dirs: Vec<_> = base_dirs
|
||||
|
||||
let data_dirs = base_dirs
|
||||
.get_data_dirs()
|
||||
.into_iter()
|
||||
.flat_map(|p| [p.join("icons"), p.join("pixmaps")])
|
||||
.collect();
|
||||
.flat_map(|p| [p.join("icons"), p.join("pixmaps")]);
|
||||
|
||||
if let Some(data_home) = base_dirs.get_data_home() {
|
||||
data_dirs.push(data_home.join("icons"));
|
||||
data_dirs.push(data_home.join("pixmaps"));
|
||||
}
|
||||
let data_home_dirs = base_dirs
|
||||
.get_data_home()
|
||||
.into_iter()
|
||||
.flat_map(|data_home| [data_home.join("icons"), data_home.join("pixmaps")].into_iter());
|
||||
|
||||
match home_dir().map(|home| home.join(".icons")) {
|
||||
Some(home_icon_dir) => data_dirs.push(home_icon_dir),
|
||||
None => tracing::warn!("No $HOME directory found"),
|
||||
}
|
||||
data_dirs.into_iter().filter(|p| p.exists()).collect()
|
||||
let home_dir = home_dir().into_iter().map(|home| home.join(".icons"));
|
||||
|
||||
data_dirs
|
||||
.chain(data_home_dirs)
|
||||
.chain(home_dir)
|
||||
.filter(|p| p.exists())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ThemePath(pub PathBuf);
|
||||
|
||||
impl ThemePath {
|
||||
pub(super) fn index(&self) -> theme::Result<PathBuf> {
|
||||
let index = self.0.join("index.theme");
|
||||
|
||||
if !index.exists() {
|
||||
return Err(ThemeError::ThemeIndexNotFound(index));
|
||||
}
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::theme::paths::icon_theme_base_paths;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue