feat(gui,core): larger icon sizes in My files

The icon grid's size slider stops at 256 px instead of 144 px, which is
big enough to tell photos apart by their thumbnail. Thumbnails are
512 px, so they stay sharp at that size on a 2x screen. Names may wrap
wider on the larger tiles.

Ctrl+plus, Ctrl+minus and Ctrl+0 step and reset the size while the grid
is shown, as they do in Photos. The size is saved in the config as
files_view.icon_size once the slider rests; configs without it keep the
72 px default.
This commit is contained in:
nvrl committed 2026-11-14 02:48:29 +01:00
1 parent 1c50797772
commit e571f22d5d
15 files changed
+3561 -3487

No files matched your search

+7
View File
@@ -224,6 +224,10 @@ pub struct FilesView {
pub descending: bool,
/// Folders ahead of files, whatever the sort.
pub folders_first: bool,
/// The icon grid's thumbnail edge in logical pixels. `None` means the
/// app's default.
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_size: Option<i32>,
}
impl Default for FilesView {
@@ -233,6 +237,7 @@ impl Default for FilesView {
sort: FileSort::Name,
descending: false,
folders_first: true,
icon_size: None,
}
}
}
@@ -777,6 +782,7 @@ mod tests {
sort: FileSort::Modified,
descending: true,
folders_first: false,
icon_size: Some(192),
},
photos_view: PhotosView {
row_height: Some(240),
@@ -856,6 +862,7 @@ mod tests {
let partial: FilesView = serde_json::from_str(r#"{"sort":"size"}"#).unwrap();
assert_eq!(partial.sort, FileSort::Size);
assert!(partial.folders_first);
assert_eq!(partial.icon_size, None);
}
#[test]
+1 -1
View File
@@ -53,7 +53,7 @@ button.sidebar-account { padding: 4px 8px; font-weight: normal; }
/* The folder on screen, last in the breadcrumb trail: a flat button's side
* padding, so the "›" before it is spaced like the ones between buttons. */
.crumb-current { padding: 0 10px; }
scale.browser-status-meter trough { min-width: 104px; min-height: 6px; }
scale.browser-status-meter trough { min-width: 140px; min-height: 6px; }
.bulk-bar { padding: 6px 8px; border-radius: 12px; background: color-mix(in srgb, var(--accent-bg-color) 12%, transparent); }
.dropzone { border: 2px dashed alpha(currentColor, 0.25); border-radius: 16px; padding: 28px 18px; transition: background 160ms ease, border-color 160ms ease; }
.dropzone-active { border-color: var(--accent-bg-color); background: color-mix(in srgb, var(--accent-bg-color) 10%, transparent); }
+19
View File
@@ -488,6 +488,7 @@ fn build_window(app: &adw::Application) {
empty_actions: browser_widgets.empty_actions.clone(),
summary: browser_widgets.summary.clone(),
zoom: browser_widgets.zoom.clone(),
zoom_save: RefCell::new(None),
},
details: DetailsState {
details: browser_widgets.details,
@@ -1523,6 +1524,9 @@ fn show_shortcuts(window: &adw::ApplicationWindow) {
("<Alt>Left <Alt>Right", gettext_noop("Back / forward")),
("<Alt>Up", gettext_noop("Parent folder")),
("<Control>1 <Control>2", gettext_noop("Grid / list")),
("<Control>plus", gettext_noop("Larger thumbnails")),
("<Control>minus", gettext_noop("Smaller thumbnails")),
("<Control>0", gettext_noop("Reset thumbnail size")),
("F2", gettext_noop("Rename")),
("Delete", gettext_noop("Move to Trash")),
("Escape", gettext_noop("Clear the selection")),
@@ -1588,6 +1592,21 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
ui.browser.search.grab_focus();
}
gtk4::gdk::Key::n | gtk4::gdk::Key::N if ctrl && on_browser => prompt_new_folder(&ui),
gtk4::gdk::Key::plus | gtk4::gdk::Key::equal | gtk4::gdk::Key::KP_Add
if ctrl && on_browser && !ui.browser.files.shows_list() =>
{
zoom_files(&ui, 1)
}
gtk4::gdk::Key::minus | gtk4::gdk::Key::KP_Subtract
if ctrl && on_browser && !ui.browser.files.shows_list() =>
{
zoom_files(&ui, -1)
}
gtk4::gdk::Key::_0 | gtk4::gdk::Key::KP_0
if ctrl && on_browser && !ui.browser.files.shows_list() =>
{
zoom_files(&ui, 0)
}
gtk4::gdk::Key::_1 | gtk4::gdk::Key::_2 if ctrl && on_browser => set_files_view(
&ui,
FilesView {
+47 -2
View File
@@ -77,6 +77,8 @@ pub(crate) struct BrowserState {
/// zoom value, and Proton account storage usage.
pub(crate) summary: gtk4::Label,
pub(crate) zoom: gtk4::Scale,
/// Saves the zoom once the slider rests, not on every step.
pub(crate) zoom_save: RefCell<Option<glib::SourceId>>,
}
/// Idle pause after the last keystroke before a search query is sent, so typing
@@ -87,9 +89,11 @@ pub(crate) const SEARCH_DEBOUNCE: Duration = Duration::from_millis(250);
pub(crate) const SEARCH_LIMIT: usize = 200;
/// Browser grid thumbnail size controlled by the bottom zoom slider. The
/// default preserves the former fixed 72 px presentation.
/// default preserves the former fixed 72 px presentation. The largest size is
/// big enough to tell photos apart by their thumbnail, and thumbnails are
/// 512 px, so they stay sharp up to it on a 2× screen.
pub(crate) const GRID_THUMB_MIN: i32 = 48;
pub(crate) const GRID_THUMB_MAX: i32 = 144;
pub(crate) const GRID_THUMB_MAX: i32 = 256;
pub(crate) const GRID_THUMB_DEFAULT: i32 = 72;
pub(crate) const GRID_THUMB_STEP: i32 = 8;
@@ -801,6 +805,7 @@ pub(crate) fn wire_browser(ui: &Rc<Ui>) {
&[],
)));
ui_zoom.browser.files.set_thumbnail_size(size);
save_icon_size(&ui_zoom, size);
});
files
@@ -1306,6 +1311,11 @@ pub(crate) fn wire_browser_actions(ui: &Rc<Ui>, build_thumbnails: &gtk4::Button)
let view = ui.dirs.load_config().files_view;
ui.browser.view.set(view);
if let Some(size) = view.icon_size {
ui.browser
.zoom
.set_value(f64::from(size.clamp(GRID_THUMB_MIN, GRID_THUMB_MAX)));
}
let layout = gio::SimpleAction::new_stateful(
"view",
Some(glib::VariantTy::STRING),
@@ -1407,6 +1417,41 @@ pub(crate) fn wire_browser_actions(ui: &Rc<Ui>, build_thumbnails: &gtk4::Button)
ui.browser.content.add_controller(drop);
}
/// Remember the grid's icon size, once the slider has rested for a moment.
fn save_icon_size(ui: &Rc<Ui>, size: i32) {
let mut view = ui.browser.view.get();
if view.icon_size == Some(size) {
return;
}
view.icon_size = Some(size);
ui.browser.view.set(view);
if let Some(source) = ui.browser.zoom_save.borrow_mut().take() {
source.remove();
}
let ui_save = ui.clone();
let source = glib::timeout_add_local_once(Duration::from_millis(500), move || {
ui_save.browser.zoom_save.borrow_mut().take();
let mut config = ui_save.dirs.load_config();
config.files_view.icon_size = ui_save.browser.view.get().icon_size;
if let Err(e) = ui_save.dirs.save_config(&config) {
tracing::warn!("cannot save the icon size: {e}");
}
});
*ui.browser.zoom_save.borrow_mut() = Some(source);
}
/// Step the grid's icon size by `steps` slider notches; `0` resets it.
pub(crate) fn zoom_files(ui: &Rc<Ui>, steps: i32) {
let zoom = &ui.browser.zoom;
let size = if steps == 0 {
GRID_THUMB_DEFAULT
} else {
(zoom.value().round() as i32 + steps * GRID_THUMB_STEP)
.clamp(GRID_THUMB_MIN, GRID_THUMB_MAX)
};
zoom.set_value(f64::from(size));
}
fn layout_name(view: FilesView) -> &'static str {
if view.list { "list" } else { "grid" }
}
+1 -1
View File
@@ -478,7 +478,7 @@ pub(crate) fn text_column(
fn resize_grid_tile(thumbnail: &gtk4::Overlay, label: &gtk4::Label, size: i32) {
resize_file_thumbnail(thumbnail, size, grid_fallback_size(size));
let name_width = (size / 6 + 1).clamp(8, 24);
let name_width = (size / 6 + 1).clamp(8, 40);
label.set_width_chars(name_width);
label.set_max_width_chars(name_width);
}
+3
View File
@@ -61,6 +61,9 @@ Existing folders remain device backups; older builds cannot open the upgraded da
- The zoom level and the tile shape are remembered between runs.
- In the photo viewer, the details panel lies over the photo when the window is
narrow instead of squeezing it.
- **Larger icons in My files.** The icon size slider goes up to 256 px, big enough to
tell photos apart by their thumbnail. Ctrl+plus, Ctrl+minus and Ctrl+0 change it from
the keyboard, and the size is remembered between runs.
### Fixed
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff
+387 -387
View File
File diff suppressed because it is too large. Load diff