From a1a1d8ac3510aefdd81aea6715e9b083e6d02b03 Mon Sep 17 00:00:00 2001 From: Infinifox Date: Sat, 1 Nov 2025 14:01:16 +1100 Subject: [PATCH 1/2] add thumbnail-mode config option --- i18n/en/cosmic_files.ftl | 6 +++ src/app.rs | 39 +++++++++++++++++++- src/config.rs | 19 ++++++++++ src/mounter/gvfs.rs | 5 ++- src/tab.rs | 79 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 137 insertions(+), 11 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 4dcaa70d..6a45a0e3 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -297,6 +297,12 @@ type-to-search = Type to Search type-to-search-recursive = Searches the current folder and all subfolders type-to-search-enter-path = Enters the path to the directory or file +### Thumbnail mode +thumbnail-mode = Thumbnails +thumbnail-mode-local = Show for local files only +thumbnail-mode-all = Show for local and network files +thumbnail-mode-never = Never show + # Context menu add-to-sidebar = Add to sidebar compress = Compress diff --git a/src/app.rs b/src/app.rs index 7ec3ce3d..43a9c996 100644 --- a/src/app.rs +++ b/src/app.rs @@ -72,7 +72,7 @@ use crate::{ clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, config::{ AppTheme, Config, DesktopConfig, Favorite, IconSizes, TIME_CONFIG_ID, TabConfig, - TimeConfig, TypeToSearch, + ThumbnailMode, TimeConfig, TypeToSearch, }, dialog::{Dialog, DialogKind, DialogMessage, DialogResult}, fl, home_dir, @@ -404,6 +404,7 @@ pub enum Message { SearchClear, SearchInput(String), SetShowDetails(bool), + SetThumbnailMode(ThumbnailMode), SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), @@ -1952,6 +1953,36 @@ impl App { Message::SetTypeToSearch, )) .into(), + widget::settings::section() + .title(fl!("thumbnail-mode")) + .add(widget::settings::item_row(vec![ + widget::radio( + widget::text::body(fl!("thumbnail-mode-local")), + ThumbnailMode::Local, + Some(self.config.thumb_cfg.thumbnail_mode), + Message::SetThumbnailMode, + ) + .into(), + ])) + .add(widget::settings::item_row(vec![ + widget::radio( + widget::text::body(fl!("thumbnail-mode-all")), + ThumbnailMode::All, + Some(self.config.thumb_cfg.thumbnail_mode), + Message::SetThumbnailMode, + ) + .into(), + ])) + .add(widget::settings::item_row(vec![ + widget::radio( + widget::text::body(fl!("thumbnail-mode-never")), + ThumbnailMode::Never, + Some(self.config.thumb_cfg.thumbnail_mode), + Message::SetThumbnailMode, + ) + .into(), + ])) + .into(), widget::settings::section() .title(fl!("other")) .add({ @@ -3728,6 +3759,12 @@ impl Application for App { config_set!(show_details, show_details); return self.update_config(); } + Message::SetThumbnailMode(thumbnail_mode) => { + let mut new_thumb_cfg = self.config.thumb_cfg.clone(); + new_thumb_cfg.thumbnail_mode = thumbnail_mode; + config_set!(thumb_cfg, new_thumb_cfg); + return self.update_config(); + } Message::SetTypeToSearch(type_to_search) => { config_set!(type_to_search, type_to_search); return self.update_config(); diff --git a/src/config.rs b/src/config.rs index d537a3fe..aa43888c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -102,6 +102,23 @@ impl Favorite { } } +/// Stores configuration for when thumbnails should be generated. +#[derive(Clone, Copy, Debug, Deserialize, Hash, Eq, PartialEq, Serialize)] +pub enum ThumbnailMode { + /// Generate thumbnails for local images only + Local, + /// Generate thumbnails for local and remote images + All, + /// Never generate thumbnails + Never, +} + +impl Default for ThumbnailMode { + fn default() -> Self { + ThumbnailMode::Local + } +} + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum TypeToSearch { Recursive, @@ -294,6 +311,7 @@ pub struct ThumbCfg { pub jobs: NonZeroU16, pub max_mem_mb: NonZeroU16, pub max_size_mb: NonZeroU16, + pub thumbnail_mode: ThumbnailMode, } impl Default for ThumbCfg { @@ -302,6 +320,7 @@ impl Default for ThumbCfg { jobs: 4.try_into().unwrap(), max_mem_mb: 2000.try_into().unwrap(), max_size_mb: 64.try_into().unwrap(), + thumbnail_mode: ThumbnailMode::default(), } } } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 4d2a44d2..57da941b 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -11,7 +11,7 @@ use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; use crate::{ config::IconSizes, err_str, - tab::{self, DirSize, ItemMetadata, ItemThumbnail, Location}, + tab::{self, DirSize, ItemLocation, ItemMetadata, Location}, }; const TARGET_URI_ATTRIBUTE: &str = "standard::target-uri"; @@ -166,7 +166,8 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: Some(ItemThumbnail::NotImage), + thumbnail_opt: None, + item_location: ItemLocation::Remote, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), diff --git a/src/tab.rs b/src/tab.rs index 41015251..1b9f4a0c 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -76,7 +76,10 @@ use crate::{ FxOrderMap, app::{Action, PreviewItem, PreviewKind}, clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste}, - config::{DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg}, + config::{ + DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg, + ThumbnailMode, + }, dialog::DialogKind, fl, localize::{LANGUAGE_SORTER, LOCALE}, @@ -679,10 +682,11 @@ pub fn item_from_gvfs_info(path: PathBuf, file_info: gio::FileInfo, sizes: IconS icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: if remote { - Some(ItemThumbnail::NotImage) + thumbnail_opt: None, + item_location: if remote { + ItemLocation::Remote } else { - None + ItemLocation::Local }, button_id: widget::Id::unique(), pos_opt: Cell::new(None), @@ -824,7 +828,12 @@ pub fn item_from_entry( icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: remote.then_some(ItemThumbnail::NotImage), + thumbnail_opt: None, + item_location: if remote { + ItemLocation::Remote + } else { + ItemLocation::Local + }, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), @@ -1045,7 +1054,7 @@ pub fn scan_search bool + Sync>( not(target_os = "android") ) )))] -pub fn scan_trash(_sizes: IconSizes) -> Vec { +pub fn scan_trash(_sizes: IconSizes, _thumbnail_mode: ThumbnailMode) -> Vec { log::warn!("viewing trash not supported on this platform"); Vec::new() } @@ -1112,7 +1121,8 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: Some(ItemThumbnail::NotImage), + thumbnail_opt: None, + item_location: ItemLocation::Trash, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), @@ -1281,7 +1291,8 @@ pub fn scan_desktop( icon_handle_grid, icon_handle_list, icon_handle_list_condensed, - thumbnail_opt: Some(ItemThumbnail::NotImage), + thumbnail_opt: None, + item_location: ItemLocation::Trash, button_id: widget::Id::unique(), pos_opt: Cell::new(None), rect_opt: Cell::new(None), @@ -1616,6 +1627,17 @@ pub enum DirSize { Error(String), } +/// Identifies the location category of an item for the purposes of generating a thumbnail. +#[derive(Clone, Debug)] +pub enum ItemLocation { + /// Item is on the local filesystem, but not in the trash + Local, + /// Item is on a remote filesystem + Remote, + /// Item is in the trash + Trash, +} + #[derive(Clone, Debug)] pub enum ItemMetadata { Path { @@ -1680,11 +1702,26 @@ impl ItemMetadata { } } +/// Represents different types of thumbnails that can be generated for file system items. +/// +/// Thumbnails are generated lazily and only for visible items to optimize performance. +/// The thumbnail generation respects memory limits and file size constraints configured +/// by the user. #[derive(Debug)] pub enum ItemThumbnail { + /// Indicates that the item cannot be thumbnailed or is not an image/previewable file. + /// This is used as the default fallback for non-previewable content. NotImage, + + /// A raster image thumbnail containing an image handle and optional original dimensions. + /// The dimensions tuple represents (width, height) of the original image if available. Image(widget::image::Handle, Option<(u32, u32)>), + + /// An SVG thumbnail containing a vector graphics handle for scalable images. Svg(widget::svg::Handle), + + /// A text preview containing the file's text content for display in a text editor widget. + /// Note: Currently disabled in the UI due to performance issues with text shaping. Text(widget::text_editor::Content), } @@ -2008,7 +2045,17 @@ pub struct Item { pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, pub icon_handle_list_condensed: widget::icon::Handle, + /// Optional thumbnail representation for this item. + /// + /// - `None`: No thumbnail has been generated yet (lazy loading) + /// - `Some(ItemThumbnail::NotImage)`: Item is not previewable + /// - `Some(ItemThumbnail::Image/Svg/Text)`: Generated thumbnail for preview + /// + /// Thumbnails are generated asynchronously only for visible items through a subscription + /// system to optimize performance and memory usage. pub thumbnail_opt: Option, + /// Describes the item location for the purposes of thumbnail generation + pub item_location: ItemLocation, pub button_id: widget::Id, pub pos_opt: Cell>, pub rect_opt: Cell>, @@ -2341,6 +2388,17 @@ impl Item { row = row.push(column); row.into() } + + /// Determines if a thumbnail may be generated, based on the thumbnail mode and item location. + pub fn allow_generate_thumbnail(&self, thumbnail_mode: &ThumbnailMode) -> bool { + match (thumbnail_mode, &self.item_location) { + (ThumbnailMode::Local, ItemLocation::Local) => true, + (ThumbnailMode::Local, ItemLocation::Remote) => false, + (ThumbnailMode::All, _) => true, + (_, ItemLocation::Trash) => false, + (ThumbnailMode::Never, _) => false, + } + } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] @@ -5582,6 +5640,11 @@ impl Tab { continue; } + if !item.allow_generate_thumbnail(&self.thumb_config.thumbnail_mode) { + // Skip generating thumbnail if not permitted for thumbnail mode + continue; + } + match item.rect_opt.get() { Some(rect) => { if !rect.intersects(&visible_rect) { From 784c3e4f0f12e453bca711b2e6654a53acdc83dc Mon Sep 17 00:00:00 2001 From: Infinifox Date: Sat, 1 Nov 2025 14:25:34 +1100 Subject: [PATCH 2/2] update tabs when thumbnail mode changed --- src/app.rs | 16 +++++++++++----- src/tab.rs | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 43a9c996..946799f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1419,11 +1419,17 @@ impl App { let tabs: Box<[_]> = self.tab_model.iter().collect(); // Update main conf and each tab with the new config let commands = std::iter::once(cosmic::command::set_theme(self.config.app_theme.theme())) - .chain(tabs.into_iter().map(|entity| { - self.update(Message::TabMessage( - Some(entity), - tab::Message::Config(self.config.tab), - )) + .chain(tabs.into_iter().flat_map(|entity| { + [ + self.update(Message::TabMessage( + Some(entity), + tab::Message::Config(self.config.tab), + )), + self.update(Message::TabMessage( + Some(entity), + tab::Message::ThumbConfig(self.config.thumb_cfg), + )), + ] })); Task::batch(commands) } diff --git a/src/tab.rs b/src/tab.rs index 1b9f4a0c..eeaacc9f 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1544,6 +1544,7 @@ pub enum Message { DoubleClick(Option), ClickRelease(Option), Config(TabConfig), + ThumbConfig(ThumbCfg), ContextAction(Action), ContextMenu(Option, Option), LocationContextMenuPoint(Option), @@ -3186,6 +3187,27 @@ impl Tab { } } } + Message::ThumbConfig(thumb_config) => { + let thumbnail_mode_changed = + self.thumb_config.thumbnail_mode != thumb_config.thumbnail_mode; + self.thumb_config = thumb_config; + if thumbnail_mode_changed { + // Reloads the tab if thumbnail mode was changed, so that when thumbnails are + // disabled the tab immediately updates to hide them. + let selected_paths = self + .selected_locations() + .into_iter() + .filter_map(Location::into_path_opt) + .collect(); + let location = self.location.clone(); + self.change_location(&location, None); + commands.push(Command::ChangeLocation( + self.title(), + location, + Some(selected_paths), + )); + } + } Message::ContextAction(action) => { // Close context menu self.context_menu = None;