From 17774af1e40c7d33cd8bb0b1fe582978e7a6d2f2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 13 Sep 2024 09:35:37 -0600 Subject: [PATCH 01/15] Track tab items by location instead of path --- src/app.rs | 16 +++--- src/dialog.rs | 4 +- src/tab.rs | 142 ++++++++++++++++++++++++++++---------------------- 3 files changed, 88 insertions(+), 74 deletions(-) diff --git a/src/app.rs b/src/app.rs index 86542c3..b5a7b34 100644 --- a/src/app.rs +++ b/src/app.rs @@ -591,7 +591,7 @@ impl App { if let Some(ref items) = tab.items_opt() { for item in items.iter() { if item.selected { - if let Some(path) = &item.path_opt { + if let Some(Location::Path(path)) = &item.location_opt { paths.push(path.clone()); } } @@ -952,7 +952,7 @@ impl App { let parent = path.parent().unwrap_or(path); for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) { - if item.path_opt.as_deref() == Some(path) { + if item.path_opt() == Some(path) { children.push(item.property_view(IconSizes::default())); } } @@ -977,7 +977,7 @@ impl App { let parent = path.parent().unwrap_or(path); for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) { - if item.path_opt.as_deref() == Some(path) { + if item.path_opt() == Some(path) { children.push(item.property_view(IconSizes::default())); } } @@ -1772,9 +1772,7 @@ impl Application for App { //TODO: this could be further optimized by looking at what exactly changed if let Some(items) = &mut tab.items_opt { for item in items.iter_mut() { - if item.path_opt.as_ref() - == Some(event_path) - { + if item.path_opt() == Some(event_path) { //TODO: reload more, like mime types? match fs::metadata(&event_path) { Ok(new_metadata) => match &mut item @@ -1836,7 +1834,7 @@ impl Application for App { if let Some(items) = tab.items_opt() { for item in items.iter() { if item.selected { - if let Some(path) = &item.path_opt { + if let Some(Location::Path(path)) = &item.location_opt { paths.push(path.clone()); } } @@ -2036,7 +2034,7 @@ impl Application for App { let mut selected = Vec::new(); for item in items.iter() { if item.selected { - if let Some(path) = &item.path_opt { + if let Some(Location::Path(path)) = &item.location_opt { selected.push(path.clone()); } } @@ -3748,7 +3746,7 @@ pub(crate) mod test_utils { name == item.name && is_dir == item.metadata.is_dir() - && path == item.path_opt.as_ref().expect("item should have path") + && path == item.path_opt().expect("item should have path") && is_hidden == item.hidden } diff --git a/src/dialog.rs b/src/dialog.rs index ced9257..7dd6dcd 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -999,7 +999,7 @@ impl Application for App { //TODO: this could be further optimized by looking at what exactly changed if let Some(items) = &mut self.tab.items_opt { for item in items.iter_mut() { - if item.path_opt.as_ref() == Some(event_path) { + if item.path_opt() == Some(event_path) { //TODO: reload more, like mime types? match fs::metadata(&event_path) { Ok(new_metadata) => { @@ -1049,7 +1049,7 @@ impl Application for App { if let Some(items) = self.tab.items_opt() { for item in items.iter() { if item.selected { - if let Some(path) = &item.path_opt { + if let Some(Location::Path(path)) = &item.location_opt { paths.push(path.clone()); let _ = update_recently_used( &path.clone(), diff --git a/src/tab.rs b/src/tab.rs index 18293e0..f919499 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -377,7 +377,7 @@ pub fn item_from_entry( display_name, metadata: ItemMetadata::Path { metadata, children }, hidden, - path_opt: Some(path), + location_opt: Some(Location::Path(path)), mime, icon_handle_grid, icon_handle_list, @@ -621,7 +621,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec { display_name, metadata: ItemMetadata::Trash { metadata, entry }, hidden: false, - path_opt: None, + location_opt: None, mime, icon_handle_grid, icon_handle_list, @@ -751,6 +751,14 @@ impl std::fmt::Display for Location { } impl Location { + pub fn path_opt(&self) -> Option<&PathBuf> { + match self { + Self::Path(path) => Some(&path), + Self::Search(path, _) => Some(&path), + _ => None, + } + } + pub fn scan(&self, sizes: IconSizes) -> Vec { match self { Self::Path(path) => scan_path(path, sizes), @@ -874,7 +882,7 @@ pub struct Item { pub display_name: String, pub metadata: ItemMetadata, pub hidden: bool, - pub path_opt: Option, + pub location_opt: Option, pub mime: Mime, pub icon_handle_grid: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle, @@ -894,6 +902,10 @@ impl Item { name.replace(".", ".\u{200B}").replace("_", "_\u{200B}") } + pub fn path_opt(&self) -> Option<&PathBuf> { + self.location_opt.as_ref()?.path_opt() + } + fn preview(&self, sizes: IconSizes) -> Element<'static, app::Message> { // This loads the image only if thumbnailing worked let icon = widget::icon::icon(self.icon_handle_grid.clone()) @@ -907,7 +919,7 @@ impl Item { { ItemThumbnail::NotImage => icon, ItemThumbnail::Rgba(_) => { - if let Some(path) = &self.path_opt { + if let Some(Location::Path(path)) = &self.location_opt { widget::image::viewer(widget::image::Handle::from_path(path)) .min_scale(1.0) .into() @@ -916,7 +928,7 @@ impl Item { } } ItemThumbnail::Svg => { - if let Some(path) = &self.path_opt { + if let Some(Location::Path(path)) = &self.location_opt { widget::Svg::from_path(path).into() } else { icon @@ -944,7 +956,7 @@ impl Item { column = column.push(widget::text(format!("Type: {}", self.mime))); - if let Some(path) = &self.path_opt { + if let Some(Location::Path(path)) = &self.location_opt { for app in self.open_with.iter() { column = column.push( widget::button( @@ -1309,10 +1321,11 @@ impl Tab { } pub fn select_path(&mut self, path: PathBuf) { + let location = Location::Path(path); *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { - item.selected = item.path_opt.as_ref() == Some(&path); + item.selected = item.location_opt.as_ref() == Some(&location); } } } @@ -1566,7 +1579,7 @@ impl Tab { .as_ref() .and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i))) { - if let Some(path) = &clicked_item.path_opt { + if let Some(Location::Path(path)) = &clicked_item.location_opt { if clicked_item.metadata.is_dir() { cd = Some(Location::Path(path.clone())); } else { @@ -1964,7 +1977,7 @@ impl Tab { if let Some(ref mut items) = self.items_opt { for item in items.iter() { if item.selected { - if let Some(path) = &item.path_opt { + if let Some(Location::Path(path)) = &item.location_opt { if path.is_dir() { //TODO: allow opening multiple tabs? cd = Some(Location::Path(path.clone())); @@ -2004,7 +2017,7 @@ impl Tab { if let Some(clicked_item) = self.items_opt.as_ref().and_then(|items| items.get(click_i)) { - if let Some(path) = &clicked_item.path_opt { + if let Some(Location::Path(path)) = &clicked_item.location_opt { if clicked_item.metadata.is_dir() { //cd = Some(Location::Path(path.clone())); commands.push(Command::OpenInNewTab(path.clone())) @@ -2035,8 +2048,9 @@ impl Tab { } Message::Thumbnail(path, thumbnail) => { if let Some(ref mut items) = self.items_opt { + let location = Location::Path(path); for item in items.iter_mut() { - if item.path_opt.as_ref() == Some(&path) { + if item.location_opt.as_ref() == Some(&location) { if let ItemThumbnail::Rgba(rgba) = &thumbnail { //TODO: pass handles already generated to avoid blocking main thread let handle = widget::icon::from_raster_pixels( @@ -2765,9 +2779,10 @@ impl Tab { } } - let column: Element = if item.metadata.is_dir() && item.path_opt.is_some() + let column: Element = if item.metadata.is_dir() + && item.location_opt.is_some() { - let tab_location = Location::Path(item.path_opt.clone().unwrap()); + let tab_location = item.location_opt.clone().unwrap(); let tab_location_enter = tab_location.clone(); let tab_location_leave = tab_location.clone(); let is_dnd_hovered = @@ -3126,58 +3141,59 @@ impl Tab { }; let button_row = button(row.into()); - let button_row: Element<_> = if item.metadata.is_dir() && item.path_opt.is_some() { - let tab_location = Location::Path(item.path_opt.clone().unwrap()); - let tab_location_enter = tab_location.clone(); - let tab_location_leave = tab_location.clone(); - let is_dnd_hovered = - self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location); - cosmic::widget::container( - DndDestination::for_data(button_row, move |data, action| { - if let Some(mut data) = data { - if action == DndAction::Copy { - Message::Drop(Some((tab_location.clone(), data))) - } else if action == DndAction::Move { - data.kind = ClipboardKind::Cut; - Message::Drop(Some((tab_location.clone(), data))) + let button_row: Element<_> = + if item.metadata.is_dir() && item.location_opt.is_some() { + let tab_location = item.location_opt.clone().unwrap(); + let tab_location_enter = tab_location.clone(); + let tab_location_leave = tab_location.clone(); + let is_dnd_hovered = + self.dnd_hovered.as_ref().map(|(l, _)| l) == Some(&tab_location); + cosmic::widget::container( + DndDestination::for_data(button_row, move |data, action| { + if let Some(mut data) = data { + if action == DndAction::Copy { + Message::Drop(Some((tab_location.clone(), data))) + } else if action == DndAction::Move { + data.kind = ClipboardKind::Cut; + Message::Drop(Some((tab_location.clone(), data))) + } else { + log::warn!("unsupported action: {:?}", action); + Message::Drop(None) + } } else { - log::warn!("unsupported action: {:?}", action); + log::warn!("No data for drop."); Message::Drop(None) } - } else { - log::warn!("No data for drop."); - Message::Drop(None) - } - }) - .on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone())) - .on_leave(move || Message::DndLeave(tab_location_leave.clone())), - ) - // todo refactor into the dnd destination wrapper - .style(if is_dnd_hovered { - theme::Container::custom(|t| { - let mut a = cosmic::iced_style::container::StyleSheet::appearance( - t, - &theme::Container::default(), - ); - let t = t.cosmic(); - // todo use theme drop target color - let mut bg = t.accent_color(); - bg.alpha = 0.2; - a.background = Some(Color::from(bg).into()); - a.border = Border { - color: t.accent_color().into(), - width: 1.0, - radius: t.radius_s().into(), - }; - a + }) + .on_enter(move |_, _, _| Message::DndEnter(tab_location_enter.clone())) + .on_leave(move || Message::DndLeave(tab_location_leave.clone())), + ) + // todo refactor into the dnd destination wrapper + .style(if is_dnd_hovered { + theme::Container::custom(|t| { + let mut a = cosmic::iced_style::container::StyleSheet::appearance( + t, + &theme::Container::default(), + ); + let t = t.cosmic(); + // todo use theme drop target color + let mut bg = t.accent_color(); + bg.alpha = 0.2; + a.background = Some(Color::from(bg).into()); + a.border = Border { + color: t.accent_color().into(), + width: 1.0, + radius: t.radius_s().into(), + }; + a + }) + } else { + theme::Container::default() }) + .into() } else { - theme::Container::default() - }) - .into() - } else { - button_row.into() - }; + button_row.into() + }; if item.selected || !drag_items.is_empty() { let dnd_row = if !item.selected { @@ -3306,8 +3322,8 @@ impl Tab { items .iter() .filter(|item| item.selected) - .filter_map(|item| item.path_opt.clone()) - .collect::>() + .filter_map(|item| item.path_opt().map(|x| x.clone())) + .collect::>() }) .unwrap_or_default(); let item_view = DndSource::<_, cosmic::app::Message, ClipboardCopy>::with_id( @@ -3485,7 +3501,7 @@ impl Tab { } } - if let Some(path) = item.path_opt.clone() { + if let Some(Location::Path(path)) = item.location_opt.clone() { subscriptions.push(subscription::channel( path.clone(), 1, From 8900966300d4f5a68433da2ca142f213684351b8 Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 01:55:30 -0400 Subject: [PATCH 02/15] chore: add test_util helper assert_scroll_affects_item_zoom --- src/app.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app.rs b/src/app.rs index b5a7b34..79a7eea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3766,6 +3766,24 @@ pub(crate) mod test_utils { ); } + pub fn assert_scroll_affects_item_zoom( + tab: &mut Tab, + message: tab::Message, + modifiers: Modifiers, + should_zoom: bool, + ) { + let grid_icon_size = tab.config.icon_sizes.grid; + let list_icon_size = tab.config.icon_sizes.list; + + debug!("Emitting {:?}", message); + tab.update(message, modifiers); + + let grid_size_changed = grid_icon_size != tab.config.icon_sizes.grid; + let list_size_changed = list_icon_size != tab.config.icon_sizes.list; + + assert_eq!(grid_size_changed || list_size_changed, should_zoom); + } + /// Assert that tab's items are equal to a path's entries. pub fn assert_eq_tab_path_contents(tab: &Tab, path: &Path) { let Location::Path(ref tab_path) = tab.location else { From 8e74a0870434ee7f00d8cfeeb75775ba8a9f50d7 Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 20:29:27 -0400 Subject: [PATCH 03/15] chore: add zoom on scroll message handling --- src/tab.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tab.rs b/src/tab.rs index f919499..bb81ebb 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -828,6 +828,8 @@ pub enum Message { ZoomDefault, ZoomIn, ZoomOut, + ScrollUp, + ScrollDown, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -1546,6 +1548,16 @@ impl Tab { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); } + Message::ScrollUp => { + if mod_ctrl { + self.update(Message::ZoomIn, modifiers); + } + } + Message::ScrollDown => { + if mod_ctrl { + self.update(Message::ZoomOut, modifiers); + } + } Message::ClickRelease(click_i_opt) => { if click_i_opt == self.clicked.take() { return commands; From 825ec0c3e344c1b2f5270908ae4d6c2d9ab1f17c Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 02:00:50 -0400 Subject: [PATCH 04/15] feat: zoom items on ctrl+scroll --- src/lib.rs | 1 + src/scroll_area.rs | 218 +++++++++++++++++++++++++++++++++++++++++++++ src/tab.rs | 76 +++++++++++++++- 3 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 src/scroll_area.rs diff --git a/src/lib.rs b/src/lib.rs index f23d24e..3af21b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ mod mime_app; pub mod mime_icon; mod mounter; mod mouse_area; +mod scroll_area; mod operation; mod spawn_detached; use tab::Location; diff --git a/src/scroll_area.rs b/src/scroll_area.rs new file mode 100644 index 0000000..cb9b709 --- /dev/null +++ b/src/scroll_area.rs @@ -0,0 +1,218 @@ +//! A container for capturing mouse wheel events. + +use cosmic::{ + iced_core::{ + event::{self, Event}, + layout, + mouse::{self}, + overlay, + renderer::{self}, + widget::{Operation, OperationOutputWrapper, Tree}, + Clipboard, Layout, Length, Rectangle, Shell, Size, Widget, + }, + widget::Id, + Element, Renderer, Theme, +}; + +/// Emit messages on mouse wheel events. +#[allow(missing_debug_implementations)] +pub struct ScrollArea<'a, Message> { + id: Id, + content: Element<'a, Message>, + on_scroll: Option) -> Option + 'a>>, + should_propogate_events: bool, +} + +impl<'a, Message> ScrollArea<'a, Message> { + /// The message to emit on a forward button release. + #[must_use] + pub fn on_scroll( + mut self, + message: impl Fn(Option) -> Option + 'a, + ) -> Self { + self.on_scroll = Some(Box::new(message)); + self + } + + /// Sets the widget's unique identifier. + #[must_use] + pub fn with_id(mut self, id: Id) -> Self { + self.id = id; + self + } +} + +impl<'a, Message> ScrollArea<'a, Message> { + /// Creates a [`ScrollArea`] with the given content. + pub fn new(content: impl Into>, should_propogate_events: bool) -> Self { + ScrollArea { + id: Id::unique(), + content: content.into(), + on_scroll: None, + should_propogate_events, + } + } +} + +impl<'a, Message> Widget for ScrollArea<'a, Message> +where + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation>, + ) { + self.content + .as_widget() + .operate(&mut tree.children[0], layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) { + return event::Status::Captured; + } + + update(self, &event, layout, cursor, shell) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.content + .as_widget_mut() + .overlay(&mut tree.children[0], layout, renderer) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message> From> for Element<'a, Message> +where + Message: 'a + Clone, + Renderer: 'a + renderer::Renderer, + Theme: 'a, +{ + fn from(area: ScrollArea<'a, Message>) -> Element<'a, Message> { + Element::new(area) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`ScrollArea`] +/// accordingly. +fn update( + widget: &mut ScrollArea<'_, Message>, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, +) -> event::Status { + let layout_bounds = layout.bounds(); + if !cursor.is_over(layout_bounds) { + return event::Status::Ignored; + } + + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + if let Some(message) = widget.on_scroll.as_ref() { + if let Some(msg) = message(Some(delta.clone())) { + shell.publish(msg); + if !widget.should_propogate_events { + return event::Status::Captured; + } + } + } + } + + event::Status::Ignored +} diff --git a/src/tab.rs b/src/tab.rs index bb81ebb..7a52108 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -63,7 +63,7 @@ use crate::{ menu, mime_app::{mime_apps, MimeApp}, mime_icon::{mime_for_path, mime_icon}, - mouse_area, + mouse_area, scroll_area::ScrollArea, }; use unix_permissions_ext::UNIXPermissionsExt; use uzers::{get_group_by_gid, get_user_by_uid}; @@ -3372,6 +3372,9 @@ impl Tab { mouse_area = mouse_area.on_right_press(Message::ContextMenu); } + let mouse_area = ScrollArea::new(mouse_area, true) + .on_scroll(respond_to_scroll_direction); + let mut popover = widget::popover(mouse_area); if let Some(point) = self.context_menu { @@ -3577,6 +3580,24 @@ impl Tab { } } +fn respond_to_scroll_direction(delta: Option) -> Option { + let delta_y = match delta { + Some(cosmic::iced_core::mouse::ScrollDelta::Lines { y, .. }) => y, + Some(cosmic::iced_core::mouse::ScrollDelta::Pixels { y, .. }) => y, + None => 0.0, + }; + + if delta_y > 0.0 { + return Some(Message::ScrollUp); + } + + if delta_y < 0.0 { + return Some(Message::ScrollDown); + } + + None +} + #[cfg(test)] mod tests { use std::{fs, io, path::PathBuf}; @@ -3589,8 +3610,9 @@ mod tests { use super::{scan_path, Location, Message, Tab}; use crate::{ app::test_utils::{ - assert_eq_tab_path, empty_fs, eq_path_item, filter_dirs, read_dir_sorted, simple_fs, - tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, + assert_eq_tab_path, assert_scroll_affects_item_zoom, empty_fs, eq_path_item, + filter_dirs, read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, + NUM_FILES, NUM_HIDDEN, NUM_NESTED, }, config::{IconSizes, TabConfig}, }; @@ -3823,6 +3845,54 @@ mod tests { Ok(()) } + #[test] + fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> { + let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; + let path = fs.path(); + + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + + let should_zoom = true; + assert_scroll_affects_item_zoom(&mut tab, Message::ScrollUp, Modifiers::CTRL, should_zoom); + + Ok(()) + } + + #[test] + fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { + let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; + let path = fs.path(); + + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + let should_not_zoom = false; + assert_scroll_affects_item_zoom(&mut tab, Message::ScrollUp, Modifiers::empty(), should_not_zoom); + + Ok(()) + } + + #[test] + fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> { + let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; + let path = fs.path(); + + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + let should_zoom = true; + assert_scroll_affects_item_zoom(&mut tab, Message::ScrollDown, Modifiers::CTRL, should_zoom); + + Ok(()) + } + + #[test] + fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { + let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; + let path = fs.path(); + + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + let should_not_zoom = false; + assert_scroll_affects_item_zoom(&mut tab, Message::ScrollDown, Modifiers::empty(), should_not_zoom); + + Ok(()) + } #[test] fn tab_empty_history_does_nothing_on_prev_next() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; From 7acda4c007d7307fb33fefbe2b920dfe9a7dddfb Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 02:05:53 -0400 Subject: [PATCH 05/15] chore: put cloths on a naked bool --- src/tab.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tab.rs b/src/tab.rs index 7a52108..3d880b1 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -3372,7 +3372,8 @@ impl Tab { mouse_area = mouse_area.on_right_press(Message::ContextMenu); } - let mouse_area = ScrollArea::new(mouse_area, true) + let should_propogate_events = true; + let mouse_area = ScrollArea::new(mouse_area, should_propogate_events) .on_scroll(respond_to_scroll_direction); let mut popover = widget::popover(mouse_area); From b332fc6b1cd2fa456e9b66992272e0d56031696e Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 08:49:28 -0400 Subject: [PATCH 06/15] chore: apply code formatting --- src/lib.rs | 2 +- src/scroll_area.rs | 24 +++++++++++++----------- src/tab.rs | 38 ++++++++++++++++++++++++++++---------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3af21b8..1a60a6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,8 @@ mod mime_app; pub mod mime_icon; mod mounter; mod mouse_area; -mod scroll_area; mod operation; +mod scroll_area; mod spawn_detached; use tab::Location; pub mod tab; diff --git a/src/scroll_area.rs b/src/scroll_area.rs index cb9b709..ac72a59 100644 --- a/src/scroll_area.rs +++ b/src/scroll_area.rs @@ -1,4 +1,4 @@ -//! A container for capturing mouse wheel events. +//! A container for reacting to scroll events. use cosmic::{ iced_core::{ @@ -14,7 +14,7 @@ use cosmic::{ Element, Renderer, Theme, }; -/// Emit messages on mouse wheel events. +/// Emit messages on scroll events. Optionally continue propogating scroll events. #[allow(missing_debug_implementations)] pub struct ScrollArea<'a, Message> { id: Id, @@ -24,7 +24,7 @@ pub struct ScrollArea<'a, Message> { } impl<'a, Message> ScrollArea<'a, Message> { - /// The message to emit on a forward button release. + /// The message to emit on a scroll. #[must_use] pub fn on_scroll( mut self, @@ -189,8 +189,7 @@ where } } -/// Processes the given [`Event`] and updates the [`State`] of a [`ScrollArea`] -/// accordingly. +/// Processes the given [`Event`] and emit any messages produced by [ScrollArea::on_scroll]. fn update( widget: &mut ScrollArea<'_, Message>, event: &Event, @@ -203,15 +202,18 @@ fn update( return event::Status::Ignored; } - if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { - if let Some(message) = widget.on_scroll.as_ref() { - if let Some(msg) = message(Some(delta.clone())) { - shell.publish(msg); - if !widget.should_propogate_events { - return event::Status::Captured; + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + if let Some(message) = on_scroll(Some(delta.clone())) { + shell.publish(message); + if !widget.should_propogate_events { + return event::Status::Captured; + } } } } + _ => {} } event::Status::Ignored diff --git a/src/tab.rs b/src/tab.rs index 3d880b1..646c298 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -63,7 +63,8 @@ use crate::{ menu, mime_app::{mime_apps, MimeApp}, mime_icon::{mime_for_path, mime_icon}, - mouse_area, scroll_area::ScrollArea, + mouse_area, + scroll_area::ScrollArea, }; use unix_permissions_ext::UNIXPermissionsExt; use uzers::{get_group_by_gid, get_user_by_uid}; @@ -3581,7 +3582,9 @@ impl Tab { } } -fn respond_to_scroll_direction(delta: Option) -> Option { +fn respond_to_scroll_direction( + delta: Option, +) -> Option { let delta_y = match delta { Some(cosmic::iced_core::mouse::ScrollDelta::Lines { y, .. }) => y, Some(cosmic::iced_core::mouse::ScrollDelta::Pixels { y, .. }) => y, @@ -3612,8 +3615,8 @@ mod tests { use crate::{ app::test_utils::{ assert_eq_tab_path, assert_scroll_affects_item_zoom, empty_fs, eq_path_item, - filter_dirs, read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, - NUM_FILES, NUM_HIDDEN, NUM_NESTED, + filter_dirs, read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, + NUM_HIDDEN, NUM_NESTED, }, config::{IconSizes, TabConfig}, }; @@ -3852,7 +3855,7 @@ mod tests { let path = fs.path(); let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); - + let should_zoom = true; assert_scroll_affects_item_zoom(&mut tab, Message::ScrollUp, Modifiers::CTRL, should_zoom); @@ -3866,7 +3869,12 @@ mod tests { let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); let should_not_zoom = false; - assert_scroll_affects_item_zoom(&mut tab, Message::ScrollUp, Modifiers::empty(), should_not_zoom); + assert_scroll_affects_item_zoom( + &mut tab, + Message::ScrollUp, + Modifiers::empty(), + should_not_zoom, + ); Ok(()) } @@ -3875,10 +3883,15 @@ mod tests { fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); - + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); let should_zoom = true; - assert_scroll_affects_item_zoom(&mut tab, Message::ScrollDown, Modifiers::CTRL, should_zoom); + assert_scroll_affects_item_zoom( + &mut tab, + Message::ScrollDown, + Modifiers::CTRL, + should_zoom, + ); Ok(()) } @@ -3887,10 +3900,15 @@ mod tests { fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); - + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); let should_not_zoom = false; - assert_scroll_affects_item_zoom(&mut tab, Message::ScrollDown, Modifiers::empty(), should_not_zoom); + assert_scroll_affects_item_zoom( + &mut tab, + Message::ScrollDown, + Modifiers::empty(), + should_not_zoom, + ); Ok(()) } From 83b8de3e722c63d75cd01ed34663ad1716753dd5 Mon Sep 17 00:00:00 2001 From: David Carvalho Date: Fri, 13 Sep 2024 13:37:32 -0300 Subject: [PATCH 07/15] pt-BR translation update --- i18n/pt-BR/cosmic_files.ftl | 38 +++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/i18n/pt-BR/cosmic_files.ftl b/i18n/pt-BR/cosmic_files.ftl index b90af80..0d34d16 100644 --- a/i18n/pt-BR/cosmic_files.ftl +++ b/i18n/pt-BR/cosmic_files.ftl @@ -4,8 +4,10 @@ empty-folder-hidden = Pasta vazia (contém itens ocultos) no-results = Nenhum item encontrado filesystem = Sistema de arquivos home = Pasta pessoal +networks = Redes notification-in-progress = Há operações de arquivo em andamento. trash = Lixeira +recents = Recentes undo = Desfazer # List view @@ -62,7 +64,6 @@ apply-to-all = Aplicar a todos keep-both = Manter ambos skip = Ignorar - ## Metadata Dialog owner = Proprietário group = Grupo @@ -76,6 +77,31 @@ execute = Execução ## About git-description = Git commit {$hash} de {$date} +## Add Network Drive +add-network-drive = Adicionar local de rede +connect = Conectar +connect-anonymously = Conectar anonimamente +connecting = Conectando... +domain = Domínio +enter-server-address = Insira o endereço do servidor +network-drive-description = + Endereços de servidor incluem um prefixo de protocolo e um endereço. + Exemplos: ssh://192.168.0.1, ftp://[2001:db8::1] +### Certifique-se de manter a vírgula que separa as colunas +network-drive-schemes = + Protocolos disponíveis,Prefixo + AppleTalk,afp:// + File Transfer Protocol,ftp:// ou ftps:// + Network File System,nfs:// + Server Message Block,smb:// + SSH File Transfer Protocol,sftp:// ou ssh:// + WebDav,dav:// ou davs:// +network-drive-error = Não foi possível acessar o local de rede +password = Senha +remember-password = Lembrar senha +try-again = Tente novamente +username = Usuário + ## Operations edit-history = Editar histórico history = Histórico @@ -130,7 +156,6 @@ restored = Restaurado {$items} {$items -> [one] item *[other] itens } da lixeira -undo = Desfazer unknown-folder = pasta desconhecida ## Open with @@ -202,3 +227,12 @@ show-hidden-files = Mostrar arquivos ocultos list-directories-first = Listar pastas primeiro menu-settings = Configurações... menu-about = Sobre o Arquivos do COSMIC... + +## Sort +sort = Ordenar +sort-a-z = A-Z +sort-z-a = Z-A +sort-newest-first = Mais novos primeiro +sort-oldest-first = Mais antigos primeiro +sort-smallest-to-largest = Do menor para o maior +sort-largest-to-smallest = Do maior para o menor From a192d93f4b90295915ec992c6c0fb47837639d0c Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Sat, 14 Sep 2024 02:11:06 -0400 Subject: [PATCH 08/15] chore: collapse scroll_area into mouse_area --- src/app.rs | 5 +- src/lib.rs | 1 - src/mouse_area.rs | 87 +++++++++++++++++- src/scroll_area.rs | 220 --------------------------------------------- src/tab.rs | 116 ++++++++++-------------- 5 files changed, 133 insertions(+), 296 deletions(-) delete mode 100644 src/scroll_area.rs diff --git a/src/app.rs b/src/app.rs index 79a7eea..e15bfd2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3766,17 +3766,16 @@ pub(crate) mod test_utils { ); } - pub fn assert_scroll_affects_item_zoom( + pub fn assert_zoom_affects_item_size( tab: &mut Tab, message: tab::Message, - modifiers: Modifiers, should_zoom: bool, ) { let grid_icon_size = tab.config.icon_sizes.grid; let list_icon_size = tab.config.icon_sizes.list; debug!("Emitting {:?}", message); - tab.update(message, modifiers); + tab.update(message, Modifiers::empty()); let grid_size_changed = grid_icon_size != tab.config.icon_sizes.grid; let list_size_changed = list_icon_size != tab.config.icon_sizes.list; diff --git a/src/lib.rs b/src/lib.rs index 1a60a6b..f23d24e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,6 @@ pub mod mime_icon; mod mounter; mod mouse_area; mod operation; -mod scroll_area; mod spawn_detached; use tab::Location; pub mod tab; diff --git a/src/mouse_area.rs b/src/mouse_area.rs index 71a049e..2de7aa7 100644 --- a/src/mouse_area.rs +++ b/src/mouse_area.rs @@ -6,8 +6,14 @@ use cosmic::{ iced_core::{ border::Border, event::{self, Event}, + keyboard::{ + self, + key::{self, Key}, + Event::{KeyPressed, KeyReleased}, + Modifiers, + }, layout, - mouse::{self, click}, + mouse::{self, click, Event as MouseEvent}, overlay, renderer::{self, Quad, Renderer as _}, touch, @@ -39,6 +45,7 @@ pub struct MouseArea<'a, Message> { on_back_release: Option) -> Message + 'a>>, on_forward_press: Option) -> Message + 'a>>, on_forward_release: Option) -> Message + 'a>>, + on_scroll: Option Option + 'a>>, show_drag_rect: bool, } @@ -144,6 +151,16 @@ impl<'a, Message> MouseArea<'a, Message> { self } + /// The message to emit on a scroll. + #[must_use] + pub fn on_scroll( + mut self, + message: impl Fn(mouse::ScrollDelta, Modifiers) -> Option + 'a, + ) -> Self { + self.on_scroll = Some(Box::new(message)); + self + } + #[must_use] pub fn show_drag_rect(mut self, show_drag_rect: bool) -> Self { self.show_drag_rect = show_drag_rect; @@ -163,7 +180,7 @@ impl<'a, Message> MouseArea<'a, Message> { struct State { // TODO: Support on_mouse_enter and on_mouse_exit drag_initiated: Option, - + modifiers: Modifiers, prev_click: Option<(mouse::Click, Instant)>, } @@ -227,6 +244,7 @@ impl<'a, Message> MouseArea<'a, Message> { on_back_release: None, on_forward_press: None, on_forward_release: None, + on_scroll: None, show_drag_rect: false, } } @@ -578,6 +596,21 @@ fn update( } } + if let Some(message) = widget.on_scroll.as_ref() { + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + if let Some(message) = on_scroll(delta.clone(), state.modifiers) { + shell.publish(message); + return event::Status::Captured; + } + } + } + } + + if let Event::Keyboard(key_event) = event { + handle_key_event(key_event, state) + }; + if let Some((message, drag_rect)) = widget.on_drag.as_ref().zip(state.drag_rect(cursor)) { shell.publish(message(drag_rect.intersection(&layout_bounds).map( |mut rect| { @@ -590,3 +623,53 @@ fn update( event::Status::Ignored } + +fn handle_key_event(key_event: &keyboard::Event, state: &mut State) { + if let KeyPressed { + key: Key::Named(key::Named::Control), + .. + } = key_event + { + state.modifiers.insert(Modifiers::CTRL); + } + + if let KeyReleased { + key: Key::Named(key::Named::Control), + .. + } = key_event + { + state.modifiers.remove(Modifiers::CTRL); + } + + if let KeyPressed { + key: Key::Named(key::Named::Shift), + .. + } = key_event + { + state.modifiers.insert(Modifiers::SHIFT); + } + + if let KeyReleased { + key: Key::Named(key::Named::Shift), + .. + } = key_event + { + state.modifiers.remove(Modifiers::SHIFT); + } + + if let KeyPressed { + key: Key::Named(key::Named::Alt), + .. + } = key_event + { + state.modifiers.insert(Modifiers::ALT); + } + + if let KeyReleased { + key: Key::Named(key::Named::Alt), + .. + } = key_event + { + state.modifiers.remove(Modifiers::ALT); + } +} diff --git a/src/scroll_area.rs b/src/scroll_area.rs deleted file mode 100644 index ac72a59..0000000 --- a/src/scroll_area.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! A container for reacting to scroll events. - -use cosmic::{ - iced_core::{ - event::{self, Event}, - layout, - mouse::{self}, - overlay, - renderer::{self}, - widget::{Operation, OperationOutputWrapper, Tree}, - Clipboard, Layout, Length, Rectangle, Shell, Size, Widget, - }, - widget::Id, - Element, Renderer, Theme, -}; - -/// Emit messages on scroll events. Optionally continue propogating scroll events. -#[allow(missing_debug_implementations)] -pub struct ScrollArea<'a, Message> { - id: Id, - content: Element<'a, Message>, - on_scroll: Option) -> Option + 'a>>, - should_propogate_events: bool, -} - -impl<'a, Message> ScrollArea<'a, Message> { - /// The message to emit on a scroll. - #[must_use] - pub fn on_scroll( - mut self, - message: impl Fn(Option) -> Option + 'a, - ) -> Self { - self.on_scroll = Some(Box::new(message)); - self - } - - /// Sets the widget's unique identifier. - #[must_use] - pub fn with_id(mut self, id: Id) -> Self { - self.id = id; - self - } -} - -impl<'a, Message> ScrollArea<'a, Message> { - /// Creates a [`ScrollArea`] with the given content. - pub fn new(content: impl Into>, should_propogate_events: bool) -> Self { - ScrollArea { - id: Id::unique(), - content: content.into(), - on_scroll: None, - should_propogate_events, - } - } -} - -impl<'a, Message> Widget for ScrollArea<'a, Message> -where - Message: Clone, -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.content)); - } - - fn size(&self) -> Size { - self.content.as_widget().size() - } - - fn layout( - &self, - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.content - .as_widget() - .layout(&mut tree.children[0], renderer, limits) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation>, - ) { - self.content - .as_widget() - .operate(&mut tree.children[0], layout, renderer, operation); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) { - return event::Status::Captured; - } - - update(self, &event, layout, cursor, shell) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - renderer_style, - layout, - cursor, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer) - } - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn set_id(&mut self, id: Id) { - self.id = id; - } -} - -impl<'a, Message> From> for Element<'a, Message> -where - Message: 'a + Clone, - Renderer: 'a + renderer::Renderer, - Theme: 'a, -{ - fn from(area: ScrollArea<'a, Message>) -> Element<'a, Message> { - Element::new(area) - } -} - -/// Processes the given [`Event`] and emit any messages produced by [ScrollArea::on_scroll]. -fn update( - widget: &mut ScrollArea<'_, Message>, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, -) -> event::Status { - let layout_bounds = layout.bounds(); - if !cursor.is_over(layout_bounds) { - return event::Status::Ignored; - } - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if let Some(on_scroll) = widget.on_scroll.as_ref() { - if let Some(message) = on_scroll(Some(delta.clone())) { - shell.publish(message); - if !widget.should_propogate_events { - return event::Status::Captured; - } - } - } - } - _ => {} - } - - event::Status::Ignored -} diff --git a/src/tab.rs b/src/tab.rs index 646c298..93b8db2 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -7,8 +7,9 @@ use cosmic::{ }, alignment::{Horizontal, Vertical}, clipboard::dnd::DndAction, + event, futures::SinkExt, - keyboard::Modifiers, + keyboard::{self, Modifiers}, subscription::{self, Subscription}, //TODO: export in cosmic::widget widget::{ @@ -24,7 +25,7 @@ use cosmic::{ Rectangle, Size, }, - iced_core::widget::tree, + iced_core::{mouse::ScrollDelta, widget::tree}, iced_style::rule, theme, widget::{ @@ -64,7 +65,6 @@ use crate::{ mime_app::{mime_apps, MimeApp}, mime_icon::{mime_for_path, mime_icon}, mouse_area, - scroll_area::ScrollArea, }; use unix_permissions_ext::UNIXPermissionsExt; use uzers::{get_group_by_gid, get_user_by_uid}; @@ -829,8 +829,6 @@ pub enum Message { ZoomDefault, ZoomIn, ZoomOut, - ScrollUp, - ScrollDown, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -1549,16 +1547,6 @@ impl Tab { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); } - Message::ScrollUp => { - if mod_ctrl { - self.update(Message::ZoomIn, modifiers); - } - } - Message::ScrollDown => { - if mod_ctrl { - self.update(Message::ZoomOut, modifiers); - } - } Message::ClickRelease(click_i_opt) => { if click_i_opt == self.clicked.take() { return commands; @@ -3365,7 +3353,8 @@ impl Tab { .on_press(move |_point_opt| Message::Click(None)) .on_release(|_| Message::ClickRelease(None)) .on_back_press(move |_point_opt| Message::GoPrevious) - .on_forward_press(move |_point_opt| Message::GoNext); + .on_forward_press(move |_point_opt| Message::GoNext) + .on_scroll(respond_to_scroll_direction); if self.context_menu.is_some() { mouse_area = mouse_area.on_right_press(move |_point_opt| Message::ContextMenu(None)); @@ -3374,9 +3363,6 @@ impl Tab { } let should_propogate_events = true; - let mouse_area = ScrollArea::new(mouse_area, should_propogate_events) - .on_scroll(respond_to_scroll_direction); - let mut popover = widget::popover(mouse_area); if let Some(point) = self.context_menu { @@ -3582,21 +3568,22 @@ impl Tab { } } -fn respond_to_scroll_direction( - delta: Option, -) -> Option { +pub fn respond_to_scroll_direction(delta: ScrollDelta, modifiers: Modifiers) -> Option { + if !modifiers.control() { + return None; + } + let delta_y = match delta { - Some(cosmic::iced_core::mouse::ScrollDelta::Lines { y, .. }) => y, - Some(cosmic::iced_core::mouse::ScrollDelta::Pixels { y, .. }) => y, - None => 0.0, + ScrollDelta::Lines { y, .. } => y, + ScrollDelta::Pixels { y, .. } => y, }; if delta_y > 0.0 { - return Some(Message::ScrollUp); + return Some(Message::ZoomIn); } if delta_y < 0.0 { - return Some(Message::ScrollDown); + return Some(Message::ZoomOut); } None @@ -3606,15 +3593,15 @@ fn respond_to_scroll_direction( mod tests { use std::{fs, io, path::PathBuf}; - use cosmic::iced_runtime::keyboard::Modifiers; + use cosmic::{iced::mouse::ScrollDelta, iced_runtime::keyboard::Modifiers}; use log::{debug, trace}; use tempfile::TempDir; use test_log::test; - use super::{scan_path, Location, Message, Tab}; + use super::{scan_path, respond_to_scroll_direction, Location, Message, Tab}; use crate::{ app::test_utils::{ - assert_eq_tab_path, assert_scroll_affects_item_zoom, empty_fs, eq_path_item, + assert_eq_tab_path, assert_zoom_affects_item_size, empty_fs, eq_path_item, filter_dirs, read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, }, @@ -3850,66 +3837,55 @@ mod tests { } #[test] - fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> { + fn tab_zoom_in_increases_item_view_size() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); - let should_zoom = true; - assert_scroll_affects_item_zoom(&mut tab, Message::ScrollUp, Modifiers::CTRL, should_zoom); + let should_affect_size = true; + assert_zoom_affects_item_size(&mut tab, Message::ZoomIn, should_affect_size); + Ok(()) + } + fn tab_zoom_out_decreases_item_view_size() -> io::Result<()> { + let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; + let path = fs.path(); + + let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); + + let should_affect_size = true; + assert_zoom_affects_item_size(&mut tab, Message::ZoomOut, should_affect_size); + Ok(()) + } + + #[test] + fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> { + let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::CTRL); + assert!(!message_maybe.is_none()); + assert!(matches!(message_maybe.unwrap(), Message::ZoomIn)); Ok(()) } #[test] fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { - let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; - let path = fs.path(); - - let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); - let should_not_zoom = false; - assert_scroll_affects_item_zoom( - &mut tab, - Message::ScrollUp, - Modifiers::empty(), - should_not_zoom, - ); - + let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::empty()); + assert!(message_maybe.is_none()); Ok(()) } #[test] fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> { - let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; - let path = fs.path(); - - let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); - let should_zoom = true; - assert_scroll_affects_item_zoom( - &mut tab, - Message::ScrollDown, - Modifiers::CTRL, - should_zoom, - ); - + let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::CTRL); + assert!(!message_maybe.is_none()); + assert!(matches!(message_maybe.unwrap(), Message::ZoomOut)); Ok(()) } #[test] fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { - let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; - let path = fs.path(); - - let mut tab = Tab::new(Location::Path(path.into()), TabConfig::default()); - let should_not_zoom = false; - assert_scroll_affects_item_zoom( - &mut tab, - Message::ScrollDown, - Modifiers::empty(), - should_not_zoom, - ); - + let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::empty()); + assert!(message_maybe.is_none()); Ok(()) } #[test] @@ -4059,7 +4035,7 @@ impl Widget for ArcElementWrapper { _clipboard: &mut dyn cosmic::iced_core::Clipboard, _shell: &mut cosmic::iced_core::Shell<'_, M>, _viewport: &Rectangle, - ) -> cosmic::iced_core::event::Status { + ) -> event::Status { self.0.lock().unwrap().as_widget_mut().on_event( _state, _event, _layout, _cursor, _renderer, _clipboard, _shell, _viewport, ) From 190e045945ebaea090a8e4ab695680ae39192753 Mon Sep 17 00:00:00 2001 From: VandaLHJ Date: Sat, 14 Sep 2024 15:24:22 +0200 Subject: [PATCH 09/15] Update cosmic_files.ftl PL translation Network Added network shares, permissions, fixed some language to feel more natural, removed dupes --- i18n/pl/cosmic_files.ftl | 44 +++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/i18n/pl/cosmic_files.ftl b/i18n/pl/cosmic_files.ftl index 267cd76..472c45e 100644 --- a/i18n/pl/cosmic_files.ftl +++ b/i18n/pl/cosmic_files.ftl @@ -4,8 +4,10 @@ empty-folder-hidden = Pusty katalog (z ukrytymi plikami) no-results = Brak wyników filesystem = System plików home = Katalog Domowy +networks = sieci notification-in-progress = Operacje na plikach w toku. trash = Kosz +recents = Ubiegłe undo = Cofnij # List view @@ -62,11 +64,44 @@ apply-to-all = Zastosuj do wszystkich keep-both = Zachowaj oba skip = Pomiń +## Metadata Dialog +owner = Właściciel +group = Grupa +other = Inni +read = Odczyt +write = Zapis +execute = Wykonywanie + # Context Pages ## About git-description = Git commit {$hash} z {$date} +## Add Network Drive +add-network-drive = Dodaj dysk sieciowy +connect = Połącz +connect-anonymously = Połącz anonimowo +connecting = Łączenie... +domain = Domena +enter-server-address = Wprowadź adres serwera +network-drive-description = + Adres serwera zawiera prefiks protokołu i adres. + Przykładowo: ssh://192.168.0.1, ftp://[2001:db8::1] +### Make sure to keep the comma which separates the columns +network-drive-schemes = + Dostępne protokoły,Prefiks + AppleTalk,afp:// + File Transfer Protocol,ftp:// or ftps:// + Network File System,nfs:// + Server Message Block,smb:// + SSH File Transfer Protocol,sftp:// or ssh:// + WebDav,dav:// or davs:// +network-drive-error = Brak dostępu do dysku sieciowego +password = Hasło +remember-password = Zapamiętaj hasło +try-again = Spróbuj ponownie +username = Nazwa użytkownika + ## Operations edit-history = Edytuj historię history = Historia @@ -200,11 +235,6 @@ show-hidden-files = Pokaż ukryte pliki list-directories-first = Najpierw wyświetlaj katalogi menu-settings = Ustawienia... menu-about = O Plikach COSMIC... -list-view = Widok listy -show-hidden-files = Pokaż ukryte pliki -list-directories-first = Najpierw wyświetlaj katalogi -menu-settings = Ustawienia... -menu-about = O Plikach COSMIC... ## Sort sort = Uszereguj @@ -212,5 +242,5 @@ sort-a-z = A-Z sort-z-a = Z-A sort-newest-first = Najpierw najnowsze sort-oldest-first = Najpierw najstarsze -sort-smallest-to-largest = Od najmniejszego do największego -sort-largest-to-smallest = Od największego do najmniejszego +sort-smallest-to-largest = Najpierw najmniejsze +sort-largest-to-smallest = Najpierw największe From 3b611ff4fc29fc09be377cec03b3c41bcbba5f82 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Sun, 15 Sep 2024 14:05:17 +0200 Subject: [PATCH 10/15] update fr translation --- i18n/fr/cosmic_files.ftl | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/i18n/fr/cosmic_files.ftl b/i18n/fr/cosmic_files.ftl index 7bfee3d..24f9b26 100644 --- a/i18n/fr/cosmic_files.ftl +++ b/i18n/fr/cosmic_files.ftl @@ -4,9 +4,9 @@ empty-folder-hidden = Dossier vide (contient des éléments cachés) no-results = Aucun résultat trouvé filesystem = Système de fichiers home = Dossier personnel -notification-in-progress = Les opérations sur les fichiers sont en cours. +notification-in-progress = Des opérations sur des fichiers sont en cours. trash = Corbeille -recents = Récente +recents = Récents undo = Annuler # List view @@ -77,6 +77,31 @@ execute = Exécution ## About git-description = Git commit {$hash} le {$date} +## Add Network Drive +add-network-drive = Ajouter un lecteur réseau +connect = Connecter +connect-anonymously = Connecter anonymement +connecting = Connection en cours... +domain = Domaine +enter-server-address = Entrez l'adresse du serveur +network-drive-description = + Les adresses de serveur incluent un préfixe de protocole et une adresse. + Exemples: ssh://192.168.0.1, ftp://[2001:db8::1] +### Make sure to keep the comma which separates the columns +network-drive-schemes = + Protocoles disponibles,Préfixe + AppleTalk,afp:// + File Transfer Protocol,ftp:// or ftps:// + Network File System,nfs:// + Server Message Block,smb:// + SSH File Transfer Protocol,sftp:// or ssh:// + WebDav,dav:// or davs:// +network-drive-error = Impossible d'accéder au lecteur réseau +password = Mot de passe +remember-password = Se souvenir du mot de passe +try-again = Essayer à nouveau +username = Nom d'utilisateur + ## Operations edit-history = Modifier l'historique history = Historique @@ -203,3 +228,12 @@ show-hidden-files = Afficher les fichiers cachés list-directories-first = Lister les répertoires en premier menu-settings = Paramètres... menu-about = À propos de Fichiers COSMIC... + +## Sort +sort = Trier +sort-a-z = A-Z +sort-z-a = Z-A +sort-newest-first = Le plus récent en premier +sort-oldest-first = Le plus ancien en premier +sort-smallest-to-largest = Du plus petit au plus grand +sort-largest-to-smallest = Du plus grand au plus petit From cd71c895f945dc3b9a2465c4074b90b04dfa7219 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 15 Sep 2024 16:00:20 -0600 Subject: [PATCH 11/15] Change display of date and time --- i18n/en/cosmic_files.ftl | 3 ++- src/tab.rs | 55 ++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index cd3a0f9..77b53b5 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -9,6 +9,7 @@ notification-in-progress = File operations are in progress. trash = Trash recents = Recents undo = Undo +today = Today # List view name = Name @@ -235,4 +236,4 @@ sort-z-a = Z-A sort-newest-first = Newest first sort-oldest-first = Oldest first sort-smallest-to-largest = Smallest to largest -sort-largest-to-smallest = Largest to smallest \ No newline at end of file +sort-largest-to-smallest = Largest to smallest diff --git a/src/tab.rs b/src/tab.rs index f919499..0252f25 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -44,7 +44,7 @@ use std::{ cell::{Cell, RefCell}, cmp::Ordering, collections::HashMap, - fmt, + fmt::{self, Display}, fs::{self, Metadata}, num::NonZeroU16, os::unix::fs::MetadataExt, @@ -72,7 +72,8 @@ pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); pub const HOVER_DURATION: Duration = Duration::from_millis(1600); //TODO: adjust for locales? -const TIME_FORMAT: &'static str = "%a %-d %b %-Y %r"; +const DATE_TIME_FORMAT: &'static str = "%b %-d, %-Y, %-I:%M %p"; +const TIME_FORMAT: &'static str = "%-I:%M %p"; static SPECIAL_DIRS: Lazy> = Lazy::new(|| { let mut special_dirs = HashMap::new(); if let Some(dir) = dirs::document_dir() { @@ -260,6 +261,31 @@ fn format_permissions(metadata: &Metadata, owner: PermissionOwner) -> String { perms.join(" ") } +struct FormatTime(std::time::SystemTime); + +impl Display for FormatTime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let date_time = chrono::DateTime::::from(self.0); + let now = chrono::Local::now(); + if date_time.date() == now.date() { + write!( + f, + "{}, {}", + fl!("today"), + date_time.format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) + ) + } else { + date_time + .format_localized(DATE_TIME_FORMAT, *LANGUAGE_CHRONO) + .fmt(f) + } + } +} + +fn format_time(time: std::time::SystemTime) -> FormatTime { + FormatTime(time) +} + #[cfg(not(target_os = "windows"))] fn hidden_attribute(_metadata: &Metadata) -> bool { false @@ -1010,27 +1036,15 @@ impl Item { } if let Ok(time) = metadata.created() { - column = column.push(widget::text(format!( - "Created: {}", - chrono::DateTime::::from(time) - .format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) - ))); + column = column.push(widget::text(format!("Created: {}", format_time(time)))); } if let Ok(time) = metadata.modified() { - column = column.push(widget::text(format!( - "Modified: {}", - chrono::DateTime::::from(time) - .format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) - ))); + column = column.push(widget::text(format!("Modified: {}", format_time(time)))); } if let Ok(time) = metadata.accessed() { - column = column.push(widget::text(format!( - "Accessed: {}", - chrono::DateTime::::from(time) - .format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) - ))); + column = column.push(widget::text(format!("Accessed: {}", format_time(time)))); } #[cfg(not(target_os = "windows"))] { @@ -1109,8 +1123,7 @@ impl Item { if let Ok(time) = metadata.modified() { column = column.push(widget::text(format!( "Last modified: {}", - chrono::DateTime::::from(time) - .format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) + format_time(time) ))); } } @@ -3051,9 +3064,7 @@ impl Tab { let modified_text = match &item.metadata { ItemMetadata::Path { metadata, .. } => match metadata.modified() { - Ok(time) => chrono::DateTime::::from(time) - .format_localized(TIME_FORMAT, *LANGUAGE_CHRONO) - .to_string(), + Ok(time) => format_time(time).to_string(), Err(_) => String::new(), }, ItemMetadata::Trash { .. } => String::new(), From c3d64980424ae752281495c63e1edcbd70773503 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 13 Sep 2024 15:13:37 -0600 Subject: [PATCH 12/15] WIP: support for network browsing --- examples/gio-list.rs | 31 ++++++++ examples/{mount.rs => gio-mount.rs} | 0 src/app.rs | 64 ++++++++-------- src/dialog.rs | 6 +- src/lib.rs | 4 + src/menu.rs | 2 +- src/mounter/gvfs.rs | 105 +++++++++++++++++++++++++- src/mounter/mod.rs | 5 +- src/operation.rs | 6 +- src/tab.rs | 113 ++++++++++++++++++---------- 10 files changed, 251 insertions(+), 85 deletions(-) create mode 100644 examples/gio-list.rs rename examples/{mount.rs => gio-mount.rs} (100%) diff --git a/examples/gio-list.rs b/examples/gio-list.rs new file mode 100644 index 0000000..a83a0d6 --- /dev/null +++ b/examples/gio-list.rs @@ -0,0 +1,31 @@ +use gio::prelude::*; +use std::env; + +fn main() { + let uri = env::args().nth(1).expect("no uri provided"); + let file = gio::File::for_uri(&uri); + for entry_res in file + .enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) + .unwrap() + { + let entry = entry_res.unwrap(); + println!("{:?}", entry.display_name()); + for attribute in entry.list_attributes(None) { + println!( + " {:?}: {:?}", + attribute, + entry.attribute_as_string(&attribute) + ); + } + + //TODO: what is the best way to resolve shortcuts? + let child = if let Some(target_uri) = + entry.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI) + { + gio::File::for_uri(&target_uri) + } else { + file.child(entry.name()) + }; + println!("{:?}", child.uri()); + } +} diff --git a/examples/mount.rs b/examples/gio-mount.rs similarity index 100% rename from examples/mount.rs rename to examples/gio-mount.rs diff --git a/src/app.rs b/src/app.rs index b5a7b34..6fa96e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -521,11 +521,14 @@ impl App { location: Location, selection_path: Option, ) -> Command { + let mounters = self.mounters.clone(); let icon_sizes = self.config.tab.icon_sizes; Command::perform( async move { let location2 = location.clone(); - match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await { + match tokio::task::spawn_blocking(move || location2.scan(mounters, icon_sizes)) + .await + { Ok(items) => { message::app(Message::TabRescan(entity, location, items, selection_path)) } @@ -672,7 +675,10 @@ impl App { .size(16) .handle(), )) - .data(Location::Networks) + .data(Location::Network( + "network:///".to_string(), + fl!("networks"), + )) .divider_above() }); @@ -941,49 +947,35 @@ impl App { fn properties(&self, entity: Option) -> Element { match entity { None => self.tab_properties(self.tab_model.active()), - Some(ContextItem::TabBar(entity)) => self.tab_properties(entity), - Some(ContextItem::NavBar(item)) => { - let mut children = Vec::new(); - + let mut children = Vec::with_capacity(1); if let Some(location) = self.nav_model.data::(item) { if let Location::Path(path) = location { - let parent = path.parent().unwrap_or(path); - - for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) { - if item.path_opt() == Some(path) { - children.push(item.property_view(IconSizes::default())); - } + //TODO: this should be done once, not when generating the view! + if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) { + children.push(item.property_view(IconSizes::default())); } - }; + } } - widget::settings::view_column(children).into() } Some(ContextItem::BreadCrumbs(index)) => { - let mut children = Vec::new(); - + let mut children = Vec::with_capacity(1); if let Some(tab) = self.tab_model.active_data::() { - let path = match tab.location { - Location::Path(ref path) => Some(path), - Location::Search(ref path, _) => Some(path), - _ => None, - } - .and_then(|path| path.ancestors().nth(index)) - .map(|path| path.to_path_buf()); - if let Some(ref path) = path { - let parent = path.parent().unwrap_or(path); - - for item in Location::Path(parent.to_owned()).scan(IconSizes::default()) { - if item.path_opt() == Some(path) { - children.push(item.property_view(IconSizes::default())); - } + let path_opt = tab + .location + .path_opt() + .and_then(|path| path.ancestors().nth(index)) + .map(|path| path.to_path_buf()); + if let Some(ref path) = path_opt { + //TODO: this should be done once, not when generating the view! + if let Ok(item) = tab::item_from_path(path, self.config.tab.icon_sizes) { + children.push(item.property_view(IconSizes::default())); } }; } - widget::settings::view_column(children).into() } } @@ -2396,11 +2388,14 @@ impl Application for App { self.toasts.remove(id); let mut paths = Vec::with_capacity(recently_trashed.len()); + let mounters = self.mounters.clone(); let icon_sizes = self.config.tab.icon_sizes; return cosmic::command::future(async move { - match tokio::task::spawn_blocking(move || Location::Trash.scan(icon_sizes)) - .await + match tokio::task::spawn_blocking(move || { + Location::Trash.scan(mounters, icon_sizes) + }) + .await { Ok(items) => { for path in &*recently_trashed { @@ -3549,6 +3544,7 @@ pub(crate) mod test_utils { use crate::{ config::{IconSizes, TabConfig}, + mounter::MounterMap, tab::Item, }; @@ -3711,7 +3707,7 @@ pub(crate) mod test_utils { // New tab with items let location = Location::Path(path.to_owned()); - let items = location.scan(IconSizes::default()); + let items = location.scan(Mounters::new(MounterMap::new()), IconSizes::default()); let mut tab = Tab::new(location, TabConfig::default()); tab.set_items(items); diff --git a/src/dialog.rs b/src/dialog.rs index 7dd6dcd..30d203a 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -376,10 +376,12 @@ struct App { impl App { fn rescan_tab(&self) -> Command { let location = self.tab.location.clone(); + let mounters = self.mounters.clone(); let icon_sizes = self.tab.config.icon_sizes; Command::perform( async move { - match tokio::task::spawn_blocking(move || location.scan(icon_sizes)).await { + match tokio::task::spawn_blocking(move || location.scan(mounters, icon_sizes)).await + { Ok(items) => message::app(Message::TabRescan(items)), Err(err) => { log::warn!("failed to rescan: {}", err); @@ -895,7 +897,7 @@ impl Application for App { } } } - DialogPage::Replace { filename } => { + DialogPage::Replace { .. } => { return self.update(Message::Save(true)); } } diff --git a/src/lib.rs b/src/lib.rs index f23d24e..0f201d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,10 @@ mod spawn_detached; use tab::Location; pub mod tab; +pub(crate) fn err_str(err: T) -> String { + err.to_string() +} + pub fn home_dir() -> PathBuf { match dirs::home_dir() { Some(home) => home, diff --git a/src/menu.rs b/src/menu.rs index 86b1d08..f3b22d6 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -197,7 +197,7 @@ pub fn context_menu<'a>( children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); } } - (_, Location::Networks) => { + (_, Location::Network(_, _)) => { //TODO: networks context menu? } (_, Location::Trash) => { diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 7ad7b4c..367eee6 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -3,10 +3,15 @@ use cosmic::{ widget, Command, }; use gio::{glib, prelude::*}; -use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc}; +use std::{any::TypeId, cell::Cell, future::pending, path::PathBuf, sync::Arc}; use tokio::sync::{mpsc, Mutex}; use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; +use crate::{ + config::IconSizes, + err_str, + tab::{self, ItemMetadata, ItemThumbnail, Location}, +}; fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { if let Some(themed_icon) = icon.downcast_ref::() { @@ -21,10 +26,97 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { None } +fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { + let mut items = Vec::new(); + let file = gio::File::for_uri(&uri); + for info_res in file + .enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) + .map_err(err_str)? + { + let info = info_res.map_err(err_str)?; + println!("{:?}", info.display_name()); + for attribute in info.list_attributes(None) { + println!( + " {:?}: {:?}: {:?}", + attribute, + info.attribute_type(&attribute), + info.attribute_as_string(&attribute) + ); + } + + let name = info.name().to_string_lossy().to_string(); + let display_name = info.display_name().to_string(); + + //TODO: what is the best way to resolve shortcuts? + let location = Location::Network( + if let Some(target_uri) = info.attribute_string(gio::FILE_ATTRIBUTE_STANDARD_TARGET_URI) + { + target_uri.to_string() + } else { + file.child(info.name()).uri().to_string() + }, + display_name.clone(), + ); + + //TODO: support dir or file + let metadata = ItemMetadata::SimpleDir { entries: 0 }; + + let (mime, icon_handle_grid, icon_handle_list, icon_handle_list_condensed) = { + let file_icon = |size| { + info.icon() + .as_ref() + .and_then(|icon| gio_icon_to_path(icon, size)) + .map(|path| widget::icon::from_path(path)) + .unwrap_or( + widget::icon::from_name(if metadata.is_dir() { + "folder" + } else { + "text-x-generic" + }) + .size(size) + .handle(), + ) + }; + ( + //TODO: get mime from content_type? + "inode/directory".parse().unwrap(), + file_icon(sizes.grid()), + file_icon(sizes.list()), + file_icon(sizes.list_condensed()), + ) + }; + + items.push(tab::Item { + name, + display_name, + metadata, + hidden: false, + location_opt: Some(location), + mime, + icon_handle_grid, + icon_handle_list, + icon_handle_list_condensed, + open_with: Vec::new(), + thumbnail_opt: Some(ItemThumbnail::NotImage), + button_id: widget::Id::unique(), + pos_opt: Cell::new(None), + rect_opt: Cell::new(None), + selected: false, + overlaps_drag_rect: false, + }); + } + Ok(items) +} + enum Cmd { Rescan, Mount(MounterItem), NetworkDrive(String), + NetworkScan( + String, + IconSizes, + mpsc::Sender, String>>, + ), Unmount(MounterItem), } @@ -267,6 +359,9 @@ impl Gvfs { } ); } + Cmd::NetworkScan(uri, sizes, items_tx) => { + items_tx.send(network_scan(&uri, sizes)).await.unwrap(); + } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Mount = item.kind else { continue }; @@ -330,6 +425,14 @@ impl Mounter for Gvfs { ) } + fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option, String>> { + let (items_tx, mut items_rx) = mpsc::channel(1); + self.command_tx + .send(Cmd::NetworkScan(uri.to_string(), sizes, items_tx)) + .unwrap(); + items_rx.blocking_recv() + } + fn unmount(&self, item: MounterItem) -> Command<()> { let command_tx = self.command_tx.clone(); Command::perform( diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index bca9622..a17dc1f 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -2,6 +2,8 @@ use cosmic::{iced::subscription, widget, Command}; use std::{collections::BTreeMap, fmt, path::PathBuf, sync::Arc}; use tokio::sync::mpsc; +use crate::{config::IconSizes, tab}; + #[cfg(feature = "gvfs")] mod gvfs; @@ -86,10 +88,11 @@ pub enum MounterMessage { NetworkResult(String, Result), } -pub trait Mounter { +pub trait Mounter: Send + Sync { //TODO: send result fn mount(&self, item: MounterItem) -> Command<()>; fn network_drive(&self, uri: String) -> Command<()>; + fn network_scan(&self, uri: &str, sizes: IconSizes) -> Option, String>>; fn unmount(&self, item: MounterItem) -> Command<()>; fn subscription(&self) -> subscription::Subscription; } diff --git a/src/operation.rs b/src/operation.rs index b5ac4e7..15dabcc 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -15,15 +15,11 @@ use walkdir::WalkDir; use crate::{ app::{ArchiveType, DialogPage, Message}, config::IconSizes, - fl, + err_str, fl, mime_icon::mime_for_path, tab, }; -fn err_str(err: T) -> String { - err.to_string() -} - fn handle_replace( msg_tx: &Arc>>, file_from: PathBuf, diff --git a/src/tab.rs b/src/tab.rs index 0252f25..b854444 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -63,6 +63,7 @@ use crate::{ menu, mime_app::{mime_apps, MimeApp}, mime_icon::{mime_for_path, mime_icon}, + mounter::Mounters, mouse_area, }; use unix_permissions_ext::UNIXPermissionsExt; @@ -561,7 +562,7 @@ pub fn scan_search(tab_path: &PathBuf, term: &str, sizes: IconSizes) -> Vec metadata.modified().ok(), - ItemMetadata::Trash { .. } => None, + _ => None, }; // Sort with latest modified first @@ -750,9 +751,17 @@ pub fn scan_recents(sizes: IconSizes) -> Vec { recents.into_iter().take(50).map(|(item, _)| item).collect() } -pub fn scan_networks(sizes: IconSizes) -> Vec { - //TODO: network folder items - vec![] +pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec { + for (key, mounter) in mounters.iter() { + match mounter.network_scan(uri, sizes) { + Some(Ok(items)) => return items, + Some(Err(err)) => { + log::warn!("failed to scan networks: {}", err); + } + None => {} + } + } + Vec::new() } #[derive(Clone, Debug, Eq, PartialEq)] @@ -761,7 +770,7 @@ pub enum Location { Search(PathBuf, String), Trash, Recents, - Networks, + Network(String, String), } impl std::fmt::Display for Location { @@ -771,7 +780,7 @@ impl std::fmt::Display for Location { Self::Search(path, term) => write!(f, "search {} for {}", path.display(), term), Self::Trash => write!(f, "trash"), Self::Recents => write!(f, "recents"), - Self::Networks => write!(f, "networks"), + Self::Network(uri, _) => write!(f, "{}", uri), } } } @@ -785,13 +794,13 @@ impl Location { } } - pub fn scan(&self, sizes: IconSizes) -> Vec { + pub fn scan(&self, mounters: Mounters, sizes: IconSizes) -> Vec { match self { Self::Path(path) => scan_path(path, sizes), Self::Search(path, term) => scan_search(path, term, sizes), Self::Trash => scan_trash(sizes), Self::Recents => scan_recents(sizes), - Self::Networks => scan_networks(sizes), + Self::Network(uri, _) => scan_network(uri, mounters, sizes), } } } @@ -881,6 +890,12 @@ pub enum ItemMetadata { metadata: trash::TrashItemMetadata, entry: trash::TrashItem, }, + SimpleDir { + entries: u64, + }, + SimpleFile { + size: u64, + }, } impl ItemMetadata { @@ -891,6 +906,8 @@ impl ItemMetadata { trash::TrashItemSize::Entries(_) => true, trash::TrashItemSize::Bytes(_) => false, }, + Self::SimpleDir { .. } => true, + Self::SimpleFile { .. } => false, } } } @@ -1087,8 +1104,8 @@ impl Item { ); } } - ItemMetadata::Trash { .. } => { - //TODO: trash metadata + _ => { + //TODO: other metadata types } } @@ -1127,8 +1144,8 @@ impl Item { ))); } } - ItemMetadata::Trash { .. } => { - //TODO: trash metadata + _ => { + //TODO: other metadata } } @@ -1278,9 +1295,7 @@ impl Tab { Location::Recents => { fl!("recents") } - Location::Networks => { - fl!("networks") - } + Location::Network(_uri, display_name) => display_name.clone(), } } @@ -1592,14 +1607,18 @@ impl Tab { .as_ref() .and_then(|items| click_i_opt.and_then(|click_i| items.get(click_i))) { - if let Some(Location::Path(path)) = &clicked_item.location_opt { + if let Some(location) = &clicked_item.location_opt { if clicked_item.metadata.is_dir() { - cd = Some(Location::Path(path.clone())); + cd = Some(location.clone()); } else { - commands.push(Command::OpenFile(path.clone())); + if let Location::Path(path) = location { + commands.push(Command::OpenFile(path.clone())); + } else { + log::warn!("no path for item {:?}", clicked_item); + } } } else { - log::warn!("no path for item {:?}", clicked_item); + log::warn!("no location for item {:?}", clicked_item); } } else { log::warn!("no item for click index {:?}", click_i_opt); @@ -1990,12 +2009,14 @@ impl Tab { if let Some(ref mut items) = self.items_opt { for item in items.iter() { if item.selected { - if let Some(Location::Path(path)) = &item.location_opt { - if path.is_dir() { + if let Some(location) = &item.location_opt { + if item.metadata.is_dir() { //TODO: allow opening multiple tabs? - cd = Some(Location::Path(path.clone())); + cd = Some(location.clone()); } else { - commands.push(Command::OpenFile(path.clone())); + if let Location::Path(path) = location { + commands.push(Command::OpenFile(path.clone())); + } } } else { //TODO: open properties? @@ -2112,20 +2133,11 @@ impl Tab { } commands.push(Command::DropFiles(to, from)) } - Location::Search(_, _) => { - log::warn!(" Copy/cut to search not supported."); - } Location::Trash if matches!(from.kind, ClipboardKind::Cut) => { commands.push(Command::MoveToTrash(from.paths)) } - Location::Trash => { - log::warn!("Copy to trash is not supported."); - } - Location::Recents => { - log::warn!("Copy to recents is not supported."); - } - Location::Networks => { - log::warn!("Copy to networks is not supported."); + _ => { + log::warn!("{:?} to {:?} is not supported.", from.kind, to); } }; } @@ -2254,6 +2266,8 @@ impl Tab { trash::TrashItemSize::Entries(entries) => (true, entries as u64), trash::TrashItemSize::Bytes(bytes) => (false, bytes), }, + ItemMetadata::SimpleDir { entries } => (true, *entries), + ItemMetadata::SimpleFile { size } => (false, *size), }; let (a_is_entry, a_size) = get_size(a.1); let (b_is_entry, b_size) = get_size(b.1); @@ -2287,7 +2301,7 @@ impl Tab { items.sort_by(|a, b| { let get_modified = |x: &Item| match &x.metadata { ItemMetadata::Path { metadata, .. } => metadata.modified().ok(), - ItemMetadata::Trash { .. } => None, + _ => None, }; let a_modified = get_modified(a.1); @@ -2588,11 +2602,14 @@ impl Tab { .into(), ); } - Location::Networks => { + Location::Network(uri, display_name) => { children.push( - widget::button(widget::text::heading(fl!("networks"))) + widget::button(widget::text::heading(display_name)) .padding(space_xxxs) - .on_press(Message::Location(Location::Networks)) + .on_press(Message::Location(Location::Network( + uri.clone(), + display_name.clone(), + ))) .style(theme::Button::Text) .into(), ); @@ -3067,13 +3084,18 @@ impl Tab { Ok(time) => format_time(time).to_string(), Err(_) => String::new(), }, - ItemMetadata::Trash { .. } => String::new(), + _ => String::new(), }; let size_text = match &item.metadata { ItemMetadata::Path { metadata, children } => { if metadata.is_dir() { - format!("{} items", children) + //TODO: translate + if *children == 1 { + format!("{} item", children) + } else { + format!("{} items", children) + } } else { format_size(metadata.len()) } @@ -3089,6 +3111,15 @@ impl Tab { } trash::TrashItemSize::Bytes(bytes) => format_size(bytes), }, + ItemMetadata::SimpleDir { entries } => { + //TODO: translate + if *entries == 1 { + format!("{} item", entries) + } else { + format!("{} items", entries) + } + } + ItemMetadata::SimpleFile { size } => format_size(*size), }; let row = if condensed { @@ -3410,7 +3441,7 @@ impl Tab { } } } - Location::Networks => { + Location::Network(uri, display_name) if uri == "network:///" => { tab_column = tab_column.push( widget::layer_container(widget::row::with_children(vec![ widget::horizontal_space(Length::Fill).into(), From 8ace8d025a879ad62b638b29e3089be937883f95 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 16 Sep 2024 09:09:21 -0600 Subject: [PATCH 13/15] Mount network path as needed --- src/menu.rs | 16 ++++- src/mounter/gvfs.rs | 148 +++++++++++++++++++++++++++----------------- src/tab.rs | 2 +- 3 files changed, 107 insertions(+), 59 deletions(-) diff --git a/src/menu.rs b/src/menu.rs index f3b22d6..dd64abb 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -198,7 +198,21 @@ pub fn context_menu<'a>( } } (_, Location::Network(_, _)) => { - //TODO: networks context menu? + if selected > 0 { + if selected_dir == 1 && selected == 1 || selected_dir == 0 { + children.push(menu_item(fl!("open"), Action::Open).into()); + } + } else { + if tab.mode.multiple() { + children.push(menu_item(fl!("select-all"), Action::SelectAll).into()); + } + if !children.is_empty() { + children.push(divider::horizontal::light().into()); + } + children.push(sort_item(fl!("sort-by-name"), HeadingOptions::Name)); + children.push(sort_item(fl!("sort-by-modified"), HeadingOptions::Modified)); + children.push(sort_item(fl!("sort-by-size"), HeadingOptions::Size)); + } } (_, Location::Trash) => { if tab.mode.multiple() { diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 367eee6..210a16b 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -27,8 +27,8 @@ fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { } fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { + let file = gio::File::for_uri(uri); let mut items = Vec::new(); - let file = gio::File::for_uri(&uri); for info_res in file .enumerate_children("*", gio::FileQueryInfoFlags::NONE, gio::Cancellable::NONE) .map_err(err_str)? @@ -108,6 +108,63 @@ fn network_scan(uri: &str, sizes: IconSizes) -> Result, String> { Ok(items) } +fn mount_op(uri: String, event_tx: mpsc::UnboundedSender) -> gio::MountOperation { + let mount_op = gio::MountOperation::new(); + mount_op.connect_ask_password( + move |mount_op, message, default_user, default_domain, flags| { + let auth = MounterAuth { + message: message.to_string(), + username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { + Some(default_user.to_string()) + } else { + None + }, + domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { + Some(default_domain.to_string()) + } else { + None + }, + password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { + Some(String::new()) + } else { + None + }, + remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) { + Some(false) + } else { + None + }, + anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) { + Some(false) + } else { + None + }, + }; + let (auth_tx, mut auth_rx) = mpsc::channel(1); + event_tx + .send(Event::NetworkAuth(uri.clone(), auth, auth_tx)) + .unwrap(); + //TODO: async recv? + if let Some(auth) = auth_rx.blocking_recv() { + if auth.anonymous_opt == Some(true) { + mount_op.set_anonymous(true); + } else { + mount_op.set_username(auth.username_opt.as_deref()); + mount_op.set_domain(auth.domain_opt.as_deref()); + mount_op.set_password(auth.password_opt.as_deref()); + if auth.remember_opt == Some(true) { + mount_op.set_password_save(gio::PasswordSave::Permanently); + } + } + mount_op.reply(gio::MountOperationResult::Handled); + } else { + mount_op.reply(gio::MountOperationResult::Aborted); + } + }, + ); + mount_op +} + enum Cmd { Rescan, Mount(MounterItem), @@ -286,62 +343,8 @@ impl Gvfs { } } Cmd::NetworkDrive(uri) => { - let mount_op = gio::MountOperation::new(); - - { - let event_tx = event_tx.clone(); - let uri = uri.clone(); - mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| { - let auth = MounterAuth { - message: message.to_string(), - username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { - Some(default_user.to_string()) - } else { - None - }, - domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { - Some(default_domain.to_string()) - } else { - None - }, - password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { - Some(String::new()) - } else { - None - }, - remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) { - Some(false) - } else { - None - }, - anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) { - Some(false) - } else { - None - } - }; - let (auth_tx, mut auth_rx) = mpsc::channel(1); - event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap(); - //TODO: async recv? - if let Some(auth) = auth_rx.blocking_recv() { - if auth.anonymous_opt == Some(true) { - mount_op.set_anonymous(true); - } else { - mount_op.set_username(auth.username_opt.as_deref()); - mount_op.set_domain(auth.domain_opt.as_deref()); - mount_op.set_password(auth.password_opt.as_deref()); - if auth.remember_opt == Some(true) { - mount_op.set_password_save(gio::PasswordSave::Permanently); - } - } - mount_op.reply(gio::MountOperationResult::Handled); - } else { - mount_op.reply(gio::MountOperationResult::Aborted); - } - }); - } - let file = gio::File::for_uri(&uri); + let mount_op = mount_op(uri.clone(), event_tx.clone()); let event_tx = event_tx.clone(); file.mount_enclosing_volume( gio::MountMountFlags::empty(), @@ -360,7 +363,38 @@ impl Gvfs { ); } Cmd::NetworkScan(uri, sizes, items_tx) => { - items_tx.send(network_scan(&uri, sizes)).await.unwrap(); + let file = gio::File::for_uri(&uri); + let needs_mount = match file.find_enclosing_mount(gio::Cancellable::NONE) { + Ok(_) => false, + Err(err) => match err.kind::() { + Some(gio::IOErrorEnum::NotMounted) => true, + _ => false + } + }; + if needs_mount { + let mount_op = mount_op(uri.clone(), event_tx.clone()); + let event_tx = event_tx.clone(); + file.mount_enclosing_volume( + gio::MountMountFlags::empty(), + Some(&mount_op), + gio::Cancellable::NONE, + move |res| { + log::info!("network scan mounted {}: result {:?}", uri, res); + items_tx.blocking_send(network_scan(&uri, sizes)).unwrap(); + event_tx.send(Event::NetworkResult(uri, match res { + Ok(()) => { + Ok(true) + }, + Err(err) => match err.kind::() { + Some(gio::IOErrorEnum::FailedHandled) => Ok(false), + _ => Err(format!("{}", err)) + } + })).unwrap(); + } + ); + } else { + items_tx.send(network_scan(&uri, sizes)).await.unwrap(); + } } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; diff --git a/src/tab.rs b/src/tab.rs index b854444..54bbc48 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -756,7 +756,7 @@ pub fn scan_network(uri: &str, mounters: Mounters, sizes: IconSizes) -> Vec return items, Some(Err(err)) => { - log::warn!("failed to scan networks: {}", err); + log::warn!("failed to scan {:?}: {}", uri, err); } None => {} } From 00e4d0a6c6aa78a55a2db6e6c9ca522b25445a5b Mon Sep 17 00:00:00 2001 From: Luna Jernberg Date: Tue, 17 Sep 2024 17:30:42 +0200 Subject: [PATCH 14/15] Update cosmic_files.ftl Update Swedish translation --- i18n/sv/cosmic_files.ftl | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/i18n/sv/cosmic_files.ftl b/i18n/sv/cosmic_files.ftl index 9c25347..fac61b1 100644 --- a/i18n/sv/cosmic_files.ftl +++ b/i18n/sv/cosmic_files.ftl @@ -2,7 +2,11 @@ empty-folder = Tom katalog empty-folder-hidden = Tom katalog (har dolda objekt) filesystem = Filsystem home = Hem +networks = Nätverk trash = Papperskorg +recents = Senaste +undo = Ångra +today = Idag # Dialog cancel = Avbryt @@ -27,6 +31,15 @@ properties = Egenskaper ## Settings settings = Inställningar +settings-tab = Flik +settings-show-hidden = Visa dolda filer +default-view = Standardvy +icon-size-list = Ikonstorlek (lista) +icon-size-grid = Ikonstorlek (rutnät) +sorting-name = Sortera efter +direction = Riktning +ascending = Stigande +descending = Fallande ### Appearance appearance = Utseende @@ -78,3 +91,39 @@ default-app = {$name} (standard) ## Show details show-details = Visa detaljer + + +network-drive-description = + Serveradresser inkluderar ett protokollprefix och en adress. + Exempel: ssh://192.168.0.1, ftp://[2001:db8::1] +### Se till att behålla kommatecken som skiljer kolumnerna åt +network-drive-schemes = + Tillgängliga protokoll, Prefix + AppleTalk,afp:// + Filöverföringsprotokoll,ftp:// eller ftps:// + Nätverksfilsystem,nfs:// + Servermeddelandeblock,smb:// + SSH-filöverföringsprotokoll,sftp:// eller ssh:// + WebDav,dav:// eller davs:// +network-drive-error = Kan inte komma åt nätverksenheten +password = Lösenord +remember-password = Kom ihåg lösenord + +## Lägg till en Nätverksenhet +add-network-drive = Lägg till en Nätverksenhet +connect = Anslut +connect-anonymously = Anslut anonymt +connecting = Ansluter... +domain = Domän +enter-server-address = Ange server address +try-again = Försök igen +username = Användarnamn + +## Sort +sort = Sortera +sort-a-z = A-Z +sort-z-a = Z-A +sort-newest-first = Nyaste först +sort-oldest-first = Äldst först +sort-smallest-to-largest = Minsta till största +sort-largest-to-smallest = Största till minsta From d39a6bfc78b78afb1d13b956667702192d8b07f2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 17 Sep 2024 10:34:08 -0600 Subject: [PATCH 15/15] Format code --- src/app.rs | 6 +----- src/tab.rs | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/app.rs b/src/app.rs index c9b8fbc..e164fd4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3762,11 +3762,7 @@ pub(crate) mod test_utils { ); } - pub fn assert_zoom_affects_item_size( - tab: &mut Tab, - message: tab::Message, - should_zoom: bool, - ) { + pub fn assert_zoom_affects_item_size(tab: &mut Tab, message: tab::Message, should_zoom: bool) { let grid_icon_size = tab.config.icon_sizes.grid; let list_icon_size = tab.config.icon_sizes.list; diff --git a/src/tab.rs b/src/tab.rs index 664ff79..3632695 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -3640,12 +3640,12 @@ mod tests { use tempfile::TempDir; use test_log::test; - use super::{scan_path, respond_to_scroll_direction, Location, Message, Tab}; + use super::{respond_to_scroll_direction, scan_path, Location, Message, Tab}; use crate::{ app::test_utils::{ - assert_eq_tab_path, assert_zoom_affects_item_size, empty_fs, eq_path_item, - filter_dirs, read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, - NUM_HIDDEN, NUM_NESTED, + assert_eq_tab_path, assert_zoom_affects_item_size, empty_fs, eq_path_item, filter_dirs, + read_dir_sorted, simple_fs, tab_click_new, NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, + NUM_NESTED, }, config::{IconSizes, TabConfig}, }; @@ -3879,7 +3879,7 @@ mod tests { } #[test] - fn tab_zoom_in_increases_item_view_size() -> io::Result<()> { + fn tab_zoom_in_increases_item_view_size() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); @@ -3890,7 +3890,7 @@ mod tests { Ok(()) } - fn tab_zoom_out_decreases_item_view_size() -> io::Result<()> { + fn tab_zoom_out_decreases_item_view_size() -> io::Result<()> { let fs = simple_fs(0, NUM_NESTED, NUM_DIRS, 0, NAME_LEN)?; let path = fs.path(); @@ -3903,7 +3903,8 @@ mod tests { #[test] fn tab_scroll_up_with_ctrl_modifier_zooms() -> io::Result<()> { - let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::CTRL); + let message_maybe = + respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::CTRL); assert!(!message_maybe.is_none()); assert!(matches!(message_maybe.unwrap(), Message::ZoomIn)); Ok(()) @@ -3911,14 +3912,16 @@ mod tests { #[test] fn tab_scroll_up_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { - let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::empty()); + let message_maybe = + respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: 1.0 }, Modifiers::empty()); assert!(message_maybe.is_none()); Ok(()) } #[test] fn tab_scroll_down_with_ctrl_modifier_zooms() -> io::Result<()> { - let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::CTRL); + let message_maybe = + respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::CTRL); assert!(!message_maybe.is_none()); assert!(matches!(message_maybe.unwrap(), Message::ZoomOut)); Ok(()) @@ -3926,7 +3929,10 @@ mod tests { #[test] fn tab_scroll_down_without_ctrl_modifier_does_not_zoom() -> io::Result<()> { - let message_maybe = respond_to_scroll_direction(ScrollDelta::Pixels { x: 0.0, y: -1.0 }, Modifiers::empty()); + let message_maybe = respond_to_scroll_direction( + ScrollDelta::Pixels { x: 0.0, y: -1.0 }, + Modifiers::empty(), + ); assert!(message_maybe.is_none()); Ok(()) }