From 8900966300d4f5a68433da2ca142f213684351b8 Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Thu, 12 Sep 2024 01:55:30 -0400 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 a192d93f4b90295915ec992c6c0fb47837639d0c Mon Sep 17 00:00:00 2001 From: Justin Gross Date: Sat, 14 Sep 2024 02:11:06 -0400 Subject: [PATCH 6/6] 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, )