diff --git a/src/app.rs b/src/app.rs index c4add2b..c1101b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -219,9 +219,12 @@ impl App { entity: segmented_button::Entity, location: Location, ) -> Command { + let icon_sizes = self.config.tab.icon_sizes; Command::perform( async move { - match tokio::task::spawn_blocking(move || location.scan()).await { + match tokio::task::spawn_blocking(move || location.scan(icon_sizes)) + .await + { Ok(items) => message::app(Message::TabRescan(entity, items)), Err(err) => { log::warn!("failed to rescan: {}", err); @@ -357,7 +360,7 @@ impl App { if let Some(ref items) = tab.items_opt { for item in items.iter() { if item.selected { - children.push(item.property_view(&self.core)); + children.push(item.property_view(&self.core, tab.config.icon_sizes)); } } } @@ -1123,7 +1126,7 @@ pub(crate) mod test_utils { use log::{debug, trace}; use tempfile::{tempdir, TempDir}; - use crate::{config::TabConfig, tab::Item}; + use crate::{config::{TabConfig, IconSizes}, tab::Item}; use super::*; @@ -1274,7 +1277,7 @@ pub(crate) mod test_utils { // New tab with items let location = Location::Path(path.to_owned()); - let items = location.scan(); + let items = location.scan(IconSizes::default()); let mut tab = Tab::new(location, TabConfig::default()); tab.items_opt = Some(items); diff --git a/src/config.rs b/src/config.rs index 4ea8000..6088333 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only +use std::num::NonZeroU16; + use cosmic::{ cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}, theme, @@ -8,6 +10,13 @@ use serde::{Deserialize, Serialize}; pub const CONFIG_VERSION: u64 = 1; +// Default icon sizes +const ICON_SIZE_DIALOG: u16 = 16; +const ICON_SIZE_LIST: u16 = 32; +const ICON_SIZE_GRID: u16 = 64; +// TODO: 5 is an arbitrary number. Maybe there's a better icon size max +const ICON_SCALE_MAX: u16 = 5; + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum AppTheme { Dark, @@ -51,14 +60,52 @@ pub struct TabConfig { pub show_hidden: bool, // TODO: Other possible options // pub sort_by: fn(&PathBuf, &PathBuf) -> Ordering, - // Icon handle sizes - // icon_size_dialog: u16, - // icon_size_list: u16, - // icon_size_grid: u16, + // Icon handle zoom percents + pub icon_sizes: IconSizes, } impl Default for TabConfig { fn default() -> Self { - Self { show_hidden: false } + Self { + show_hidden: false, + icon_sizes: IconSizes::default(), + } + } +} + +macro_rules! percent { + ($perc:expr, $pixel:ident) => { + (($perc.get() as f32 * $pixel as f32) / 100.).clamp(1., ($pixel * ICON_SCALE_MAX) as _) + }; +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, CosmicConfigEntry, Deserialize, Serialize)] +pub struct IconSizes { + pub dialog: NonZeroU16, + pub list: NonZeroU16, + pub grid: NonZeroU16, +} + +impl Default for IconSizes { + fn default() -> Self { + Self { + dialog: 100.try_into().unwrap(), + list: 100.try_into().unwrap(), + grid: 100.try_into().unwrap(), + } + } +} + +impl IconSizes { + pub fn dialog(&self) -> u16 { + percent!(self.dialog, ICON_SIZE_DIALOG) as _ + } + + pub fn list(&self) -> u16 { + percent!(self.list, ICON_SIZE_LIST) as _ + } + + pub fn grid(&self) -> u16 { + percent!(self.grid, ICON_SIZE_GRID) as _ } } diff --git a/src/dialog.rs b/src/dialog.rs index d818cd6..630fe7f 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -209,9 +209,10 @@ struct App { impl App { fn rescan_tab(&self) -> Command { let location = self.tab.location.clone(); + let icon_sizes = self.tab.config.icon_sizes; Command::perform( async move { - match tokio::task::spawn_blocking(move || location.scan()).await { + match tokio::task::spawn_blocking(move || location.scan(icon_sizes)).await { Ok(items) => message::app(Message::TabRescan(items)), Err(err) => { log::warn!("failed to rescan: {}", err); diff --git a/src/tab.rs b/src/tab.rs index 2270e7a..b73f34a 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -23,13 +23,14 @@ use std::{ time::{Duration, Instant}, }; -use crate::{config::TabConfig, dialog::DialogKind, fl, mime_icon::mime_icon}; +use crate::{ + config::{IconSizes, TabConfig}, + dialog::DialogKind, + fl, + mime_icon::mime_icon, +}; const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); -//TODO: configurable -const ICON_SIZE_DIALOG: u16 = 16; -const ICON_SIZE_LIST: u16 = 32; -const ICON_SIZE_GRID: u16 = 64; static SPECIAL_DIRS: Lazy> = Lazy::new(|| { let mut special_dirs = HashMap::new(); if let Some(dir) = dirs::document_dir() { @@ -187,7 +188,7 @@ fn open_command(path: &PathBuf) -> process::Command { command } -pub fn scan_path(tab_path: &PathBuf) -> Vec { +pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec { let mut items = Vec::new(); match fs::read_dir(tab_path) { Ok(entries) => { @@ -228,19 +229,18 @@ pub fn scan_path(tab_path: &PathBuf) -> Vec { let path = entry.path(); - //TODO: configurable size let (icon_handle_dialog, icon_handle_grid, icon_handle_list) = if metadata.is_dir() { ( - folder_icon(&path, ICON_SIZE_DIALOG), - folder_icon(&path, ICON_SIZE_GRID), - folder_icon(&path, ICON_SIZE_LIST), + folder_icon(&path, sizes.dialog()), + folder_icon(&path, sizes.grid()), + folder_icon(&path, sizes.list()), ) } else { ( - mime_icon(&path, ICON_SIZE_DIALOG), - mime_icon(&path, ICON_SIZE_GRID), - mime_icon(&path, ICON_SIZE_LIST), + mime_icon(&path, sizes.dialog()), + mime_icon(&path, sizes.grid()), + mime_icon(&path, sizes.list()), ) }; @@ -307,7 +307,7 @@ pub fn scan_trash() -> Vec { not(target_os = "android") ) ))] -pub fn scan_trash() -> Vec { +pub fn scan_trash(sizes: IconSizes) -> Vec { let mut items: Vec = Vec::new(); match trash::os_limited::list() { Ok(entries) => { @@ -323,17 +323,16 @@ pub fn scan_trash() -> Vec { let path = entry.original_path(); let name = entry.name.clone(); - //TODO: configurable size let (icon_handle_dialog, icon_handle_grid, icon_handle_list) = match metadata.size { trash::TrashItemSize::Entries(_) => ( - folder_icon(&path, ICON_SIZE_DIALOG), - folder_icon(&path, ICON_SIZE_GRID), - folder_icon(&path, ICON_SIZE_LIST), + folder_icon(&path, sizes.dialog()), + folder_icon(&path, sizes.grid()), + folder_icon(&path, sizes.list()), ), trash::TrashItemSize::Bytes(_) => ( - mime_icon(&path, ICON_SIZE_DIALOG), - mime_icon(&path, ICON_SIZE_GRID), - mime_icon(&path, ICON_SIZE_LIST), + mime_icon(&path, sizes.dialog()), + mime_icon(&path, sizes.grid()), + mime_icon(&path, sizes.list()), ), }; @@ -369,10 +368,10 @@ pub enum Location { } impl Location { - pub fn scan(&self) -> Vec { + pub fn scan(&self, sizes: IconSizes) -> Vec { match self { - Self::Path(path) => scan_path(path), - Self::Trash => scan_trash(), + Self::Path(path) => scan_path(path, sizes), + Self::Trash => scan_trash(sizes), } } } @@ -429,11 +428,11 @@ pub struct Item { } impl Item { - pub fn property_view(&self, core: &Core) -> Element { + pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element { let mut section = widget::settings::view_section(""); section = section.add(widget::settings::item::item_row(vec![ widget::icon::icon(self.icon_handle_list.clone()) - .size(ICON_SIZE_LIST) + .size(sizes.list()) .into(), widget::text(self.name.clone()).into(), ])); @@ -869,7 +868,10 @@ impl Tab { //TODO: get from config let item_width = Length::Fixed(96.0); let item_height = Length::Fixed(116.0); - let TabConfig { show_hidden } = self.config; + let TabConfig { + show_hidden, + icon_sizes, + } = self.config; let mut children: Vec> = Vec::new(); if let Some(ref items) = self.items_opt { @@ -884,7 +886,7 @@ impl Tab { let button = widget::button( widget::column::with_children(vec![ widget::icon::icon(item.icon_handle_grid.clone()) - .size(ICON_SIZE_GRID) + .size(icon_sizes.grid()) .into(), widget::text(item.name.clone()).into(), ]) @@ -951,7 +953,10 @@ impl Tab { if let Some(ref items) = self.items_opt { let mut count = 0; let mut hidden = 0; - let TabConfig { show_hidden } = self.config; + let TabConfig { + show_hidden, + icon_sizes, + } = self.config; for (i, item) in items.iter().enumerate() { if !show_hidden && item.hidden { hidden += 1; @@ -998,11 +1003,11 @@ impl Tab { widget::row::with_children(vec![ if self.dialog.is_some() { widget::icon::icon(item.icon_handle_dialog.clone()) - .size(ICON_SIZE_DIALOG) + .size(icon_sizes.dialog()) .into() } else { widget::icon::icon(item.icon_handle_list.clone()) - .size(ICON_SIZE_LIST) + .size(icon_sizes.list()) .into() }, widget::text(item.name.clone()).width(Length::Fill).into(), @@ -1071,7 +1076,7 @@ mod tests { read_dir_sorted, simple_fs, sort_files, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, }, - config::TabConfig, + config::{IconSizes, TabConfig}, }; // Boilerplate for tab tests. Checks if simulated clicks selected items. @@ -1153,7 +1158,7 @@ mod tests { let entries = read_dir_sorted(path)?; debug!("Calling scan_path(\"{}\")", path.display()); - let actual = scan_path(&path.to_owned()); + let actual = scan_path(&path.to_owned(), IconSizes::default()); // scan_path shouldn't skip any entries assert_eq!(entries.len(), actual.len()); @@ -1177,7 +1182,7 @@ mod tests { assert!(!invalid_path.exists()); debug!("Calling scan_path(\"{}\")", invalid_path.display()); - let actual = scan_path(&invalid_path); + let actual = scan_path(&invalid_path, IconSizes::default()); assert!(actual.is_empty()); @@ -1190,7 +1195,7 @@ mod tests { let path = fs.path(); debug!("Calling scan_path(\"{}\")", path.display()); - let actual = scan_path(&path.to_owned()); + let actual = scan_path(&path.to_owned(), IconSizes::default()); assert_eq!(0, path.read_dir()?.count()); assert!(actual.is_empty());