diff --git a/Cargo.lock b/Cargo.lock index 83a6667..ae9078f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -941,9 +941,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345c78335be0624ed29012dc10c49102196c6882c12dde65d9f35b02da2aada8" +checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" dependencies = [ "smallvec", "target-lexicon", @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1231,7 +1231,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "quote", "syn 1.0.109", @@ -1317,7 +1317,7 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.12.1" -source = "git+https://github.com/pop-os/cosmic-text.git#e8f567cf5b456dfab749a575c257acaa36f622d9" +source = "git+https://github.com/pop-os/cosmic-text.git#4fe90bb6126c22f589b46768d7754d65ae300c5e" dependencies = [ "bitflags 2.6.0", "fontdb", @@ -1340,7 +1340,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "almost", "cosmic-config", @@ -2783,7 +2783,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "dnd", "iced_accessibility", @@ -2802,7 +2802,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "accesskit", "accesskit_unix", @@ -2812,7 +2812,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "bitflags 2.6.0", "dnd", @@ -2834,7 +2834,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "futures", "iced_core", @@ -2847,7 +2847,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "bitflags 2.6.0", "bytemuck", @@ -2871,7 +2871,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2883,7 +2883,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "dnd", "iced_accessibility", @@ -2897,7 +2897,7 @@ dependencies = [ [[package]] name = "iced_sctk" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "enum-repr", "float-cmp", @@ -2924,7 +2924,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "iced_core", "once_cell", @@ -2934,7 +2934,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "bytemuck", "cosmic-text", @@ -2951,7 +2951,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "as-raw-xcb-connection", "bitflags 2.6.0", @@ -2980,7 +2980,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "dnd", "iced_accessibility", @@ -2998,7 +2998,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "dnd", "iced_accessibility", @@ -3507,14 +3507,14 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#ddb678ca6966dfdf10911feac5e5ac02d3b2b97e" +source = "git+https://github.com/pop-os/libcosmic.git#701638009df09a254b7d077ddc4d1076cd87a147" dependencies = [ "apply", "ashpd 0.9.1", @@ -4563,9 +4563,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" @@ -5591,9 +5591,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.2" +version = "7.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "070a0a5e7da2d24be457809c4b3baa57a835fd2829ad8b86f9a049052fe71031" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" dependencies = [ "cfg-expr", "heck 0.5.0", @@ -5693,18 +5693,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/src/app.rs b/src/app.rs index 5d1049e..a44b318 100644 --- a/src/app.rs +++ b/src/app.rs @@ -157,7 +157,7 @@ impl Action { Action::MoveToTrash => Message::MoveToTrash(entity_opt), Action::NewFile => Message::NewItem(entity_opt, false), Action::NewFolder => Message::NewItem(entity_opt, true), - Action::Open => Message::TabMessage(entity_opt, tab::Message::Open), + Action::Open => Message::TabMessage(entity_opt, tab::Message::Open(None)), Action::OpenInNewTab => Message::OpenInNewTab(entity_opt), Action::OpenInNewWindow => Message::OpenInNewWindow(entity_opt), Action::OpenItemLocation => Message::OpenItemLocation(entity_opt), @@ -342,7 +342,7 @@ pub enum ContextPage { } impl ContextPage { - fn title(&self) -> String { + pub fn title(&self) -> String { match self { Self::About => String::new(), Self::EditHistory => fl!("edit-history"), @@ -969,14 +969,14 @@ impl App { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); match kind { PreviewKind::Custom(PreviewItem(item)) => { - children.push(item.property_view(IconSizes::default())); + children.push(item.preview_view(IconSizes::default())); } PreviewKind::Location(location) => { if let Some(tab) = self.tab_model.data::(entity) { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.location_opt.as_ref() == Some(location) { - children.push(item.property_view(tab.config.icon_sizes)); + children.push(item.preview_view(tab.config.icon_sizes)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -990,7 +990,7 @@ impl App { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.selected { - children.push(item.property_view(tab.config.icon_sizes)); + children.push(item.preview_view(tab.config.icon_sizes)); // Only show one property view to avoid issues like hangs when generating // preview images on thousands of files break; @@ -1360,6 +1360,14 @@ impl Application for App { return Command::none(); } + // Close gallery mode if open + if let Some(tab) = self.tab_model.data_mut::(entity) { + if tab.gallery { + tab.gallery = false; + return Command::none(); + } + } + // Close menus and context panes in order per message // Why: It'd be weird to close everything all at once // Usually, the Escape key (for example) closes menus and panes one by one instead @@ -2392,6 +2400,12 @@ impl Application for App { tab::Command::PreviewCancel => { self.preview_opt = None; } + tab::Command::WindowDrag => { + commands.push(window::drag(self.main_window_id())); + } + tab::Command::WindowToggleMaximize => { + commands.push(window::toggle_maximize(self.main_window_id())); + } } } return Command::batch(commands); @@ -2772,6 +2786,17 @@ impl Application for App { } fn dialog(&self) -> Option> { + //TODO: should gallery view just be a dialog? + let entity = self.tab_model.active(); + if let Some(tab) = self.tab_model.data::(entity) { + if tab.gallery { + return Some( + tab.gallery_view() + .map(move |tab_message| Message::TabMessage(Some(entity), tab_message)), + ); + } + } + let dialog_page = match self.dialog_pages.front() { Some(some) => some, None => return None, @@ -3226,6 +3251,7 @@ impl Application for App { } else { elements.push( widget::button::icon(widget::icon::from_name("system-search-symbolic")) + .padding(8) .on_press(Message::SearchActivate) .into(), ) diff --git a/src/dialog.rs b/src/dialog.rs index a98a182..5a1c39b 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -39,8 +39,8 @@ use std::{ }; use crate::{ - app::{Action, Message as AppMessage}, - config::{Config, Favorite, TabConfig}, + app::{Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind}, + config::{Config, Favorite, IconSizes, TabConfig}, fl, home_dir, localize::LANGUAGE_SORTER, menu, @@ -315,6 +315,7 @@ enum Message { NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), Open, + Preview(PreviewKind, time::Duration), Save(bool), SearchActivate, SearchClear, @@ -324,6 +325,18 @@ enum Message { TabRescan(Vec), } +impl From for Message { + fn from(app_message: AppMessage) -> Message { + match app_message { + AppMessage::TabMessage(_entity_opt, tab_message) => Message::TabMessage(tab_message), + unsupported => { + log::warn!("{unsupported:?} not supported in dialog mode"); + Message::None + } + } + } +} + pub struct MounterData(MounterKey, MounterItem); struct WatcherWrapper { @@ -355,6 +368,7 @@ struct App { title: String, accept_label: String, choices: Vec, + context_page: ContextPage, dialog_pages: VecDeque, dialog_text_input: widget::Id, filters: Vec, @@ -364,6 +378,7 @@ struct App { mounters: Mounters, mounter_items: HashMap, nav_model: segmented_button::SingleSelectModel, + preview_opt: Option<(PreviewKind, time::Instant)>, result_opt: Option, search_active: bool, search_id: widget::Id, @@ -374,6 +389,96 @@ struct App { } impl App { + fn button_row(&self) -> Element { + let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; + + let mut row = widget::row::with_capacity( + if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3, + ) + .align_items(Alignment::Center) + .padding(space_xxs) + .spacing(space_xxs); + if !self.filters.is_empty() { + row = row.push(widget::dropdown( + &self.filters, + self.filter_selected, + Message::Filter, + )); + } + for (choice_i, choice) in self.choices.iter().enumerate() { + match choice { + DialogChoice::CheckBox { label, value, .. } => { + row = row.push(widget::checkbox(label, *value, move |checked| { + Message::Choice(choice_i, if checked { 1 } else { 0 }) + })); + } + DialogChoice::ComboBox { + label, + options, + selected, + .. + } => { + row = row.push(widget::text::heading(label)); + row = row.push(widget::dropdown(options, *selected, move |option_i| { + Message::Choice(choice_i, option_i) + })); + } + } + } + if let DialogKind::SaveFile { filename } = &self.flags.kind { + row = row.push( + widget::text_input("", filename) + .id(self.filename_id.clone()) + .on_input(Message::Filename) + .on_submit(Message::Save(false)), + ); + } else { + row = row.push(widget::horizontal_space(Length::Fill)); + } + row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel)); + row = row.push(if self.flags.kind.save() { + widget::button::suggested(&self.accept_label).on_press(Message::Save(false)) + } else { + widget::button::suggested(&self.accept_label).on_press(Message::Open) + }); + + row.into() + } + + fn preview(&self, kind: &PreviewKind) -> Element { + let mut children = Vec::with_capacity(1); + match kind { + PreviewKind::Custom(PreviewItem(item)) => { + children.push(item.preview_view(IconSizes::default())); + } + PreviewKind::Location(location) => { + if let Some(items) = self.tab.items_opt() { + for item in items.iter() { + if item.location_opt.as_ref() == Some(location) { + children.push(item.preview_view(self.tab.config.icon_sizes)); + // Only show one property view to avoid issues like hangs when generating + // preview images on thousands of files + break; + } + } + } + } + PreviewKind::Selected => { + if let Some(items) = self.tab.items_opt() { + for item in items.iter() { + if item.selected { + children.push(item.preview_view(self.tab.config.icon_sizes)); + // Only show one property view to avoid issues like hangs when generating + // preview images on thousands of files + break; + } + } + } + } + } + widget::settings::view_column(children).into() + } + fn rescan_tab(&self) -> Command { let location = self.tab.location.clone(); let mounters = self.mounters.clone(); @@ -606,6 +711,7 @@ impl Application for App { title, accept_label, choices: Vec::new(), + context_page: ContextPage::Settings, dialog_pages: VecDeque::new(), dialog_text_input: widget::Id::unique(), filters: Vec::new(), @@ -615,6 +721,7 @@ impl Application for App { mounters: mounters(), mounter_items: HashMap::new(), nav_model: segmented_button::ModelBuilder::default().build(), + preview_opt: None, result_opt: None, search_active: false, search_id: widget::Id::unique(), @@ -638,7 +745,33 @@ impl Application for App { self.flags.window_id } + fn context_drawer(&self) -> Option> { + if !self.core.window.show_context { + return None; + } + + match &self.context_page { + ContextPage::Preview(_, kind) => Some(self.preview(kind).map(Message::from)), + _ => None, + } + } + fn dialog(&self) -> Option> { + //TODO: should gallery view just be a dialog? + if self.tab.gallery { + return Some( + widget::column::with_children(vec![ + self.tab.gallery_view().map(Message::TabMessage), + // Draw button row as part of the overlay + widget::container(self.button_row()) + .width(Length::Fill) + .style(theme::Container::WindowBackground) + .into(), + ]) + .into(), + ); + } + let dialog_page = match self.dialog_pages.front() { Some(some) => some, None => return None, @@ -752,15 +885,7 @@ impl Application for App { elements.push( menu::dialog_menu(&self.tab, &self.key_binds) - .map(|message| match message { - AppMessage::TabMessage(_entity_opt, tab_message) => { - Message::TabMessage(tab_message) - } - unsupported => { - log::warn!("{unsupported:?} not supported in dialog mode"); - Message::None - } - }) + .map(Message::from) .into(), ); @@ -823,6 +948,12 @@ impl Application for App { } fn on_escape(&mut self) -> Command { + if self.tab.gallery { + // Close gallery if open + self.tab.gallery = false; + return Command::none(); + } + if self.search_active { // Close search if open self.search_active = false; @@ -1100,6 +1231,17 @@ impl Application for App { } } } + Message::Preview(kind, timeout) => { + if self + .preview_opt + .as_ref() + .is_some_and(|(k, i)| *k == kind && i.elapsed() > timeout) + { + self.context_page = ContextPage::Preview(None, kind); + self.set_show_context(true); + self.set_context_title(self.context_page.title()); + } + } Message::Save(replace) => { if let DialogKind::SaveFile { filename } = &self.flags.kind { if !filename.is_empty() { @@ -1176,14 +1318,9 @@ impl Application for App { let mut commands = Vec::new(); for tab_command in tab_commands { match tab_command { - tab::Command::Action(action) => match action.message() { - AppMessage::TabMessage(_entity_opt, tab_message) => { - commands.push(self.update(Message::TabMessage(tab_message))); - } - unsupported => { - log::warn!("{unsupported:?} not supported in dialog mode"); - } - }, + tab::Command::Action(action) => { + commands.push(self.update(Message::from(action.message()))); + } tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_path) => { commands .push(Command::batch([self.update_watcher(), self.rescan_tab()])); @@ -1202,6 +1339,29 @@ impl Application for App { commands.push(self.update(Message::Open)); } } + tab::Command::Preview(kind, mut timeout) => { + self.preview_opt = Some((kind.clone(), time::Instant::now())); + if self.core.window.show_context { + // If the context window is already open, immediately show the preview + timeout = time::Duration::new(0, 0) + }; + commands.push(Command::perform( + async move { + tokio::time::sleep(timeout).await; + message::app(Message::Preview(kind, timeout)) + }, + |x| x, + )); + } + tab::Command::PreviewCancel => { + self.preview_opt = None; + } + tab::Command::WindowDrag => { + commands.push(window::drag(self.main_window_id())); + } + tab::Command::WindowToggleMaximize => { + commands.push(window::toggle_maximize(self.main_window_id())); + } unsupported => { log::warn!("{unsupported:?} not supported in dialog mode"); } @@ -1282,9 +1442,8 @@ impl Application for App { /// Creates a view after each update. fn view(&self) -> Element { - let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; - let mut tab_column = widget::column::with_capacity(2); + tab_column = tab_column.push( //TODO: key binds for dialog self.tab @@ -1292,57 +1451,7 @@ impl Application for App { .map(move |message| Message::TabMessage(message)), ); - let mut row = widget::row::with_capacity( - if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3, - ) - .align_items(Alignment::Center) - .padding(space_xxs) - .spacing(space_xxs); - if !self.filters.is_empty() { - row = row.push(widget::dropdown( - &self.filters, - self.filter_selected, - Message::Filter, - )); - } - for (choice_i, choice) in self.choices.iter().enumerate() { - match choice { - DialogChoice::CheckBox { label, value, .. } => { - row = row.push(widget::checkbox(label, *value, move |checked| { - Message::Choice(choice_i, if checked { 1 } else { 0 }) - })); - } - DialogChoice::ComboBox { - label, - options, - selected, - .. - } => { - row = row.push(widget::text::heading(label)); - row = row.push(widget::dropdown(options, *selected, move |option_i| { - Message::Choice(choice_i, option_i) - })); - } - } - } - if let DialogKind::SaveFile { filename } = &self.flags.kind { - row = row.push( - widget::text_input("", filename) - .id(self.filename_id.clone()) - .on_input(Message::Filename) - .on_submit(Message::Save(false)), - ); - } else { - row = row.push(widget::horizontal_space(Length::Fill)); - } - row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel)); - row = row.push(if self.flags.kind.save() { - widget::button::suggested(&self.accept_label).on_press(Message::Save(false)) - } else { - widget::button::suggested(&self.accept_label).on_press(Message::Open) - }); - - tab_column = tab_column.push(row); + tab_column = tab_column.push(self.button_row()); let content: Element<_> = tab_column.into(); diff --git a/src/menu.rs b/src/menu.rs index 1cc8739..36d187b 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -283,7 +283,8 @@ pub fn dialog_menu<'a>( widget::button::icon(widget::icon::from_name(match tab.config.view { tab::View::Grid => "view-grid-symbolic", tab::View::List => "view-list-symbolic", - })), + })) + .padding(8), menu::items( key_binds, vec![ @@ -305,7 +306,8 @@ pub fn dialog_menu<'a>( "view-sort-ascending-symbolic" } else { "view-sort-descending-symbolic" - })), + })) + .padding(8), menu::items( key_binds, vec![ diff --git a/src/tab.rs b/src/tab.rs index 36742b7..b7d1c1c 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -818,6 +818,8 @@ pub enum Command { OpenInNewWindow(PathBuf), Preview(PreviewKind, Duration), PreviewCancel, + WindowDrag, + WindowToggleMaximize, } #[derive(Clone, Debug)] @@ -837,6 +839,9 @@ pub enum Message { EditLocation(Option), OpenInNewTab(PathBuf), EmptyTrash, + Gallery(bool), + GalleryPrevious, + GalleryNext, GoNext, GoPrevious, ItemDown, @@ -845,7 +850,7 @@ pub enum Message { ItemUp, Location(Location), LocationUp, - Open, + Open(Option), RightClick(Option), MiddleClick(usize), Scroll(Viewport), @@ -861,6 +866,8 @@ pub enum Message { DndHover(Location), DndEnter(Location), DndLeave(Location), + WindowDrag, + WindowToggleMaximize, ZoomDefault, ZoomIn, ZoomOut, @@ -916,7 +923,7 @@ impl ItemMetadata { #[derive(Clone, Debug)] pub enum ItemThumbnail { NotImage, - Rgba(image::RgbaImage), + Rgba(image::RgbaImage, (u32, u32)), Svg, } @@ -962,11 +969,9 @@ impl Item { .unwrap_or(&ItemThumbnail::NotImage) { ItemThumbnail::NotImage => icon, - ItemThumbnail::Rgba(_) => { + ItemThumbnail::Rgba(_, _) => { if let Some(Location::Path(path)) = &self.location_opt { - widget::image::viewer(widget::image::Handle::from_path(path)) - .min_scale(1.0) - .into() + widget::image(widget::image::Handle::from_path(path)).into() } else { icon } @@ -1025,10 +1030,41 @@ impl Item { column.into() } - pub fn property_view(&self, sizes: IconSizes) -> Element<'static, app::Message> { - let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing; + pub fn preview_view(&self, sizes: IconSizes) -> Element<'static, app::Message> { + let cosmic_theme::Spacing { + space_xxxs, + space_xxs, + space_m, + .. + } = theme::active().cosmic().spacing; - let mut column = widget::column().spacing(space_xxxs); + let mut column = widget::column().spacing(space_m); + + let mut row = widget::row::with_capacity(3).spacing(space_xxs); + row = row.push( + widget::button::icon(widget::icon::from_name("go-previous-symbolic")) + .on_press(app::Message::TabMessage(None, Message::ItemLeft)), + ); + row = row.push( + widget::button::icon(widget::icon::from_name("go-next-symbolic")) + .on_press(app::Message::TabMessage(None, Message::ItemRight)), + ); + match self + .thumbnail_opt + .as_ref() + .unwrap_or(&ItemThumbnail::NotImage) + { + ItemThumbnail::NotImage => {} + ItemThumbnail::Rgba(_, _) | ItemThumbnail::Svg => { + if let Some(path) = self.path_opt() { + row = row.push( + widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic")) + .on_press(app::Message::TabMessage(None, Message::Gallery(true))), + ); + } + } + } + column = column.push(row); column = column.push(widget::row::with_children(vec![ widget::horizontal_space(Length::Fill).into(), @@ -1036,79 +1072,97 @@ impl Item { widget::horizontal_space(Length::Fill).into(), ])); - column = column.push(widget::text::heading(self.name.clone())); - - column = column.push(widget::text(format!("Type: {}", self.mime))); - + let mut details = widget::column().spacing(space_xxxs); + details = details.push(widget::text::heading(self.name.clone())); + details = details.push(widget::text(format!("Type: {}", self.mime))); + let mut settings = Vec::new(); //TODO: translate! //TODO: correct display of folder size? match &self.metadata { ItemMetadata::Path { metadata, children } => { if metadata.is_dir() { - column = column.push(widget::text(format!("Items: {}", children))); + details = details.push(widget::text(format!("Items: {}", children))); } else { - column = column.push(widget::text(format!( + details = details.push(widget::text(format!( "Size: {}", format_size(metadata.len()) ))); } if let Ok(time) = metadata.created() { - column = column.push(widget::text(format!("Created: {}", format_time(time)))); + details = details.push(widget::text(format!("Created: {}", format_time(time)))); } if let Ok(time) = metadata.modified() { - column = column.push(widget::text(format!("Modified: {}", format_time(time)))); + details = + details.push(widget::text(format!("Modified: {}", format_time(time)))); } if let Ok(time) = metadata.accessed() { - column = column.push(widget::text(format!("Accessed: {}", format_time(time)))); + details = + details.push(widget::text(format!("Accessed: {}", format_time(time)))); } + #[cfg(not(target_os = "windows"))] { - column = column.push( - widget::Row::new() - .push(widget::text(format!("{}:", fl!("owner")))) - .push(widget::text(format_permissions_owner( - metadata, - PermissionOwner::Owner, - ))) - .push(widget::text(format!( - "({})", - format_permissions(metadata, PermissionOwner::Owner,) - ))) - .spacing(10), + settings.push( + widget::settings::item::builder(format_permissions_owner( + metadata, + PermissionOwner::Owner, + )) + .description(fl!("owner")) + .control(widget::text(format_permissions( + metadata, + PermissionOwner::Owner, + ))), ); - column = column.push( - widget::Row::new() - .push(widget::text(format!("{}:", fl!("group")))) - .push(widget::text(format_permissions_owner( - metadata, - PermissionOwner::Group, - ))) - .push(widget::text(format!( - "({})", - format_permissions(metadata, PermissionOwner::Group,) - ))) - .spacing(10), + settings.push( + widget::settings::item::builder(format_permissions_owner( + metadata, + PermissionOwner::Group, + )) + .description(fl!("group")) + .control(widget::text(format_permissions( + metadata, + PermissionOwner::Group, + ))), ); - column = column.push( - widget::Row::new() - .push(widget::text(format!("{}", fl!("other")))) - .push(widget::text(format!( - "({})", - format_permissions(metadata, PermissionOwner::Other,) - ))) - .spacing(10), - ); + settings.push(widget::settings::item::builder(fl!("other")).control( + widget::text(format_permissions(metadata, PermissionOwner::Other)), + )); } } _ => { //TODO: other metadata types } } + match self + .thumbnail_opt + .as_ref() + .unwrap_or(&ItemThumbnail::NotImage) + { + ItemThumbnail::Rgba(_, (width, height)) => { + details = details.push(widget::text(format!("{}x{}", width, height))); + } + _ => {} + } + column = column.push(details); + + if let Some(path) = self.path_opt() { + column = column.push(widget::button::standard(fl!("open")).on_press( + app::Message::TabMessage(None, Message::Open(Some(path.to_path_buf()))), + )); + } + + if !settings.is_empty() { + let mut section = widget::settings::section(); + for setting in settings { + section = section.add(setting); + } + column = column.push(section); + } column.into() } @@ -1222,6 +1276,7 @@ pub struct Tab { pub history_i: usize, pub history: Vec, pub config: TabConfig, + pub gallery: bool, pub(crate) items_opt: Option>, pub dnd_hovered: Option<(Location, Instant)>, scrollable_id: widget::Id, @@ -1269,6 +1324,7 @@ impl Tab { history_i: 0, history, config, + gallery: false, items_opt: None, scrollable_id: widget::Id::unique(), select_focus: None, @@ -1851,6 +1907,48 @@ impl Tab { Message::EmptyTrash => { commands.push(Command::EmptyTrash); } + Message::Gallery(gallery) => { + self.gallery = gallery; + } + Message::GalleryPrevious | Message::GalleryNext => { + let mut pos_opt = None; + if let Some(mut indices) = self.column_sort() { + if matches!(message, Message::GalleryPrevious) { + indices.reverse(); + } + let mut found = false; + for (index, item) in indices { + if self.select_focus == None { + found = true; + } + if self.select_focus == Some(index) { + found = true; + continue; + } + if found { + if item.mime.type_() == mime::IMAGE { + pos_opt = item.pos_opt.get(); + if pos_opt.is_some() { + break; + } + } + } + } + } + if let Some((row, col)) = pos_opt { + // Should mod_shift be available? + self.select_position(row, col, mod_shift); + } + if let Some(offset) = self.select_focus_scroll() { + commands.push(Command::Iced(scrollable::scroll_to( + self.scrollable_id.clone(), + offset, + ))); + } + if let Some(id) = self.select_focus_id() { + commands.push(Command::Iced(widget::button::focus(id))); + } + } Message::GoNext => { if let Some(history_i) = self.history_i.checked_add(1) { if let Some(location) = self.history.get(history_i) { @@ -2031,21 +2129,32 @@ impl Tab { } } } - Message::Open => { - if let Some(ref mut items) = self.items_opt { - for item in items.iter() { - if item.selected { - if let Some(location) = &item.location_opt { - if item.metadata.is_dir() { - //TODO: allow opening multiple tabs? - cd = Some(location.clone()); - } else { - if let Location::Path(path) = location { - commands.push(Command::OpenFile(path.clone())); + Message::Open(path_opt) => { + match path_opt { + Some(path) => { + if path.is_dir() { + cd = Some(Location::Path(path)); + } else { + commands.push(Command::OpenFile(path)); + } + } + None => { + if let Some(ref mut items) = self.items_opt { + for item in items.iter() { + if item.selected { + if let Some(location) = &item.location_opt { + if item.metadata.is_dir() { + //TODO: allow opening multiple tabs? + cd = Some(location.clone()); + } else { + if let Location::Path(path) = location { + commands.push(Command::OpenFile(path.clone())); + } + } + } else { + //TODO: open properties? } } - } else { - //TODO: open properties? } } } @@ -2121,7 +2230,7 @@ impl Tab { let location = Location::Path(path); for item in items.iter_mut() { if item.location_opt.as_ref() == Some(&location) { - if let ItemThumbnail::Rgba(rgba) = &thumbnail { + if let ItemThumbnail::Rgba(rgba, _) = &thumbnail { //TODO: pass handles already generated to avoid blocking main thread let handle = widget::icon::from_raster_pixels( rgba.width(), @@ -2210,6 +2319,12 @@ impl Tab { self.dnd_hovered = None; } } + Message::WindowDrag => { + commands.push(Command::WindowDrag); + } + Message::WindowToggleMaximize => { + commands.push(Command::WindowToggleMaximize); + } Message::ZoomDefault => match self.config.view { View::List => self.config.icon_sizes.list = 100.try_into().unwrap(), View::Grid => self.config.icon_sizes.grid = 100.try_into().unwrap(), @@ -2436,6 +2551,116 @@ impl Tab { .into() } + pub fn gallery_view(&self) -> Element { + let cosmic_theme::Spacing { + space_xxs, + space_xs, + space_m, + .. + } = theme::active().cosmic().spacing; + + //TODO: display error messages when image not found? + let mut name_opt = None; + let mut image_opt: Option> = None; + if let Some(index) = self.select_focus { + if let Some(items) = &self.items_opt { + if let Some(item) = items.get(index) { + name_opt = Some(widget::text::heading(&item.display_name)); + match item + .thumbnail_opt + .as_ref() + .unwrap_or(&ItemThumbnail::NotImage) + { + ItemThumbnail::NotImage => {} + ItemThumbnail::Rgba(_, _) => { + if let Some(path) = item.path_opt() { + image_opt = Some( + //TODO: use widget::image::viewer, when its zoom can be reset + widget::image(widget::image::Handle::from_path(path)) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ); + } + } + ItemThumbnail::Svg => { + if let Some(path) = item.path_opt() { + image_opt = Some( + widget::Svg::from_path(path) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ); + } + } + } + } + } + } + + let mut column = widget::column::with_capacity(2); + column = column.push(widget::vertical_space(Length::Fixed(space_m.into()))); + { + let mut row = widget::row::with_capacity(5).align_items(Alignment::Center); + row = row.push(widget::horizontal_space(Length::Fill)); + if let Some(name) = name_opt { + row = row.push(name); + } + row = row.push(widget::horizontal_space(Length::Fill)); + row = row.push( + widget::button::icon(widget::icon::from_name("window-close-symbolic")) + .style(theme::Button::Standard) + .on_press(Message::Gallery(false)), + ); + row = row.push(widget::horizontal_space(Length::Fixed(space_m.into()))); + // This mouse area provides window drag while the header bar is hidden + let mouse_area = mouse_area::MouseArea::new(row) + .on_drag(|_| Message::WindowDrag) + .on_double_click(|_| Message::WindowToggleMaximize); + column = column.push(mouse_area); + } + { + let mut row = widget::row::with_capacity(7).align_items(Alignment::Center); + row = row.push(widget::horizontal_space(Length::Fixed(space_m.into()))); + row = row.push( + widget::button::icon(widget::icon::from_name("go-previous-symbolic")) + .padding(space_xs) + .style(theme::Button::Standard) + .on_press(Message::GalleryPrevious), + ); + row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into()))); + if let Some(image) = image_opt { + row = row.push(image); + } else { + //TODO: what to do when no image? + row = row.push(widget::Space::new(Length::Fill, Length::Fill)); + } + row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into()))); + row = row.push( + widget::button::icon(widget::icon::from_name("go-next-symbolic")) + .padding(space_xs) + .style(theme::Button::Standard) + .on_press(Message::GalleryNext), + ); + row = row.push(widget::horizontal_space(Length::Fixed(space_m.into()))); + column = column.push(row); + } + + widget::container(column) + .width(Length::Fill) + .height(Length::Fill) + .style(theme::Container::Custom(Box::new(|theme| { + let cosmic = theme.cosmic(); + let mut bg = cosmic.bg_color(); + bg.alpha = 0.75; + widget::container::Appearance { + background: Some(Color::from(bg).into()), + ..Default::default() + } + }))) + .into() + } + pub fn location_view(&self) -> Element { //TODO: responsiveness is done in a hacky way, potentially move this to a custom widget? fn text_width<'a>( @@ -2533,7 +2758,7 @@ impl Tab { _ => {} } //TODO: make it possible to resize with the mouse - return crate::mouse_area::MouseArea::new(row) + return mouse_area::MouseArea::new(row) .on_press(move |_point_opt| Message::ToggleSort(msg)) .into(); }; @@ -3644,7 +3869,10 @@ impl Tab { (ICON_SIZE_GRID * ICON_SCALE_MAX) as u32; let thumbnail = image.thumbnail(thumbnail_size, thumbnail_size); - ItemThumbnail::Rgba(thumbnail.to_rgba8()) + ItemThumbnail::Rgba( + thumbnail.to_rgba8(), + (image.width(), image.height()), + ) } Err(err) => { log::warn!("failed to decode {:?}: {}", path, err);