Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions i18n/en/cosmic_files.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 49 additions & 6 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -404,6 +404,7 @@ pub enum Message {
SearchClear,
SearchInput(String),
SetShowDetails(bool),
SetThumbnailMode(ThumbnailMode),
SetTypeToSearch(TypeToSearch),
SystemThemeModeChange,
Size(window::Id, Size),
Expand Down Expand Up @@ -1418,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)
}
Expand Down Expand Up @@ -1952,6 +1959,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({
Expand Down Expand Up @@ -3728,6 +3765,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();
Expand Down
19 changes: 19 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/mounter/gvfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -166,7 +166,8 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result<Vec<tab::Item>, 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),
Expand Down
101 changes: 93 additions & 8 deletions src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -1045,7 +1054,7 @@ pub fn scan_search<F: Fn(&Path, &str, Metadata) -> bool + Sync>(
not(target_os = "android")
)
)))]
pub fn scan_trash(_sizes: IconSizes) -> Vec<Item> {
pub fn scan_trash(_sizes: IconSizes, _thumbnail_mode: ThumbnailMode) -> Vec<Item> {
log::warn!("viewing trash not supported on this platform");
Vec::new()
}
Expand Down Expand Up @@ -1112,7 +1121,8 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -1533,6 +1544,7 @@ pub enum Message {
DoubleClick(Option<usize>),
ClickRelease(Option<usize>),
Config(TabConfig),
ThumbConfig(ThumbCfg),
ContextAction(Action),
ContextMenu(Option<Point>, Option<window::Id>),
LocationContextMenuPoint(Option<Point>),
Expand Down Expand Up @@ -1616,6 +1628,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 {
Expand Down Expand Up @@ -1680,11 +1703,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),
}

Expand Down Expand Up @@ -2008,7 +2046,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<ItemThumbnail>,
/// Describes the item location for the purposes of thumbnail generation
pub item_location: ItemLocation,
pub button_id: widget::Id,
pub pos_opt: Cell<Option<(usize, usize)>>,
pub rect_opt: Cell<Option<Rectangle>>,
Expand Down Expand Up @@ -2341,6 +2389,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)]
Expand Down Expand Up @@ -3128,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;
Expand Down Expand Up @@ -5582,6 +5662,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) {
Expand Down