diff --git a/Cargo.lock b/Cargo.lock index 9b8f289..227b27d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1484,6 +1484,7 @@ dependencies = [ "bzip2", "chrono", "compio", + "cosmic-client-toolkit", "cosmic-mime-apps", "dirs 6.0.0", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 940e636..0b62331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ icu = { version = "1.5.0", features = [ "compiled_data", "icu_datetime_experimental", ] } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "178eb0b", optional = true } cosmic-mime-apps = { git = "https://github.com/pop-os/cosmic-mime-apps.git", optional = true } dirs = "6.0.0" env_logger = "0.11" @@ -106,7 +107,7 @@ io-uring = ["compio/io-uring", "dep:io-uring"] io-uring-bindgen = ["io-uring?/bindgen"] jemalloc = ["dep:tikv-jemallocator"] notify = ["dep:notify-rust"] -wayland = ["libcosmic/wayland", "dep:wayland-client"] +wayland = ["libcosmic/wayland", "dep:cctk", "dep:wayland-client"] wgpu = ["libcosmic/wgpu"] [profile.dev] diff --git a/src/app.rs b/src/app.rs index 7cff634..8acd804 100644 --- a/src/app.rs +++ b/src/app.rs @@ -602,11 +602,12 @@ pub struct MounterData(MounterKey, MounterItem); #[derive(Clone, Debug)] pub enum WindowKind { + ContextMenu(Entity, widget::Id), Desktop(Entity), DesktopViewOptions, Dialogs(widget::Id), - Preview(Option, PreviewKind), FileDialog(Option>), + Preview(Option, PreviewKind), } pub struct WatcherWrapper { @@ -671,7 +672,7 @@ pub struct App { surface_names: HashMap, toasts: widget::toaster::Toasts, watcher_opt: Option<(Debouncer, HashSet)>, - window_id_opt: Option, + pub(crate) window_id_opt: Option, windows: HashMap, nav_dnd_hover: Option<(Location, Instant)>, tab_dnd_hover: Option<(Entity, Instant)>, @@ -1120,9 +1121,20 @@ impl App { } fn remove_window(&mut self, id: &window::Id) { - if let Some(WindowKind::Desktop(entity)) = self.windows.remove(id) { - // Remove the tab from the tab model - self.tab_model.remove(entity); + if let Some(window_kind) = self.windows.remove(id) { + match window_kind { + WindowKind::ContextMenu(entity, _) => { + // Close context menu + if let Some(tab) = self.tab_model.data_mut::(entity) { + tab.context_menu = None; + } + } + WindowKind::Desktop(entity) => { + // Remove the tab from the tab model + self.tab_model.remove(entity); + } + _ => {} + } } } @@ -2410,8 +2422,10 @@ impl Application for App { } if let Some(tab) = self.tab_model.data_mut::(entity) { if tab.context_menu.is_some() { - tab.context_menu = None; - return Task::none(); + return self.update(Message::TabMessage( + Some(entity), + tab::Message::ContextMenu(None), + )); } if tab.edit_location.is_some() { @@ -3683,12 +3697,26 @@ impl Application for App { return self.update_config(); } Message::TabActivate(entity) => { - self.tab_model.activate(entity); + let mut tasks = Vec::new(); + // Close old context menu + let active = self.tab_model.active(); + if let Some(tab) = self.tab_model.data_mut::(active) { + if tab.context_menu.is_some() { + tasks.push(self.update(Message::TabMessage( + Some(active), + tab::Message::ContextMenu(None), + ))); + } + } + + // Activate new tab + self.tab_model.activate(entity); if let Some(tab) = self.tab_model.data::(entity) { self.activate_nav_model_location(&tab.location.clone()); } - return self.update_title(); + tasks.push(self.update_title()); + return Task::batch(tasks); } Message::TabNext => { let len = self.tab_model.iter().count(); @@ -3824,6 +3852,75 @@ impl Application for App { self.update_tab(entity, tab_path, selection_paths), ])); } + tab::Command::ContextMenu(point_opt) => { + #[cfg(feature = "wayland")] + match point_opt { + Some(point) => { + if crate::is_wayland() { + // Open context menu + use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{ + Anchor, Gravity, + }; + use cosmic::iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let window_id = WindowId::unique(); + self.windows.insert( + window_id.clone(), + WindowKind::ContextMenu(entity, widget::Id::unique()), + ); + commands.push(self.update(Message::Surface( + cosmic::surface::action::app_popup( + move |app: &mut crate::App| -> SctkPopupSettings { + let anchor_rect = Rectangle { + x: point.x as i32, + y: point.y as i32, + width: 1, + height: 1, + }; + let positioner = SctkPositioner { + size: None, + anchor_rect, + anchor: Anchor::None, + gravity: Gravity::BottomRight, + reactive: true, + ..Default::default() + }; + SctkPopupSettings { + parent: app + .window_id_opt + .unwrap_or_else(|| WindowId::NONE), + id: window_id, + positioner, + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + } + }, + None, + ), + ))); + } + } + None => { + // Destroy previous popup + let mut window_ids = Vec::new(); + for (window_id, window_kind) in self.windows.iter() { + if let WindowKind::ContextMenu(e, _) = window_kind { + if *e == entity { + window_ids.push(*window_id); + } + } + } + for window_id in window_ids { + commands.push(self.update(Message::Surface( + cosmic::surface::action::destroy_popup(window_id), + ))); + } + } + } + } tab::Command::Delete(paths) => commands.push(self.delete(paths)), tab::Command::DropFiles(to, from) => { commands.push(self.update(Message::PasteContents(to, from))); @@ -4055,10 +4152,12 @@ impl Application for App { } } Message::WindowUnfocus => { + /*TODO let tab_entity = self.tab_model.active(); if let Some(tab) = self.tab_model.data_mut::(tab_entity) { tab.context_menu = None; } + */ } Message::WindowCloseRequested(id) => { self.remove_window(&id); @@ -4548,9 +4647,9 @@ impl Application for App { }; } } - Message::Surface(a) => { + Message::Surface(action) => { return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), + cosmic::app::Action::Surface(action), )); } Message::SaveSortNames => { @@ -5583,6 +5682,19 @@ impl Application for App { fn view_window(&self, id: WindowId) -> Element { let content = match self.windows.get(&id) { + Some(WindowKind::ContextMenu(entity, id)) => { + match self.tab_model.data::(*entity) { + Some(tab) => { + return widget::autosize::autosize( + menu::context_menu(tab, &self.key_binds, &self.modifiers) + .map(|x| Message::TabMessage(Some(*entity), x)), + id.clone(), + ) + .into() + } + None => widget::text("Unknown tab ID").into(), + } + } Some(WindowKind::Desktop(entity)) => { let mut tab_column = widget::column::with_capacity(3); diff --git a/src/dialog.rs b/src/dialog.rs index b01c9a4..78820ad 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -457,6 +457,7 @@ impl From for Message { AppMessage::ZoomIn(_entity_opt) => Message::ZoomIn, AppMessage::ZoomOut(_entity_opt) => Message::ZoomOut, AppMessage::NewItem(_entity_opt, true) => Message::NewFolder, + AppMessage::Surface(action) => Message::Surface(action), unsupported => { log::warn!("{unsupported:?} not supported in dialog mode"); Message::None @@ -1236,8 +1237,7 @@ impl Application for App { } if self.tab.context_menu.is_some() { - self.tab.context_menu = None; - return Task::none(); + return self.update(Message::TabMessage(tab::Message::ContextMenu(None))); } if self.tab.edit_location.is_some() { @@ -1787,9 +1787,9 @@ impl Application for App { tab::View::Grid => zoom_out(&mut config.icon_sizes.grid, 50, 500), }); } - Message::Surface(a) => { + Message::Surface(action) => { return cosmic::task::message(cosmic::Action::Cosmic( - cosmic::app::Action::Surface(a), + cosmic::app::Action::Surface(action), )); } } diff --git a/src/lib.rs b/src/lib.rs index da06bfe..8f285ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,13 @@ pub fn home_dir() -> PathBuf { } } +pub fn is_wayland() -> bool { + matches!( + cosmic::app::cosmic::windowing_system(), + Some(cosmic::app::cosmic::WindowingSystem::Wayland) + ) +} + /// Runs application in desktop mode #[rustfmt::skip] pub fn desktop() -> Result<(), Box> { diff --git a/src/mouse_area.rs b/src/mouse_area.rs index 9aa38e9..ff16a3b 100644 --- a/src/mouse_area.rs +++ b/src/mouse_area.rs @@ -32,7 +32,8 @@ pub struct MouseArea<'a, Message> { on_release: Option>>, on_resize: Option>>, on_right_press: Option>>, - on_right_press_no_capture: Option>>, + on_right_press_no_capture: bool, + on_right_press_window_position: bool, on_right_release: Option>>, on_middle_press: Option>>, on_middle_release: Option>>, @@ -103,10 +104,17 @@ impl<'a, Message> MouseArea<'a, Message> { self } - /// The message to emit on a right button press without capturing. + /// on_right_press will not capture input #[must_use] - pub fn on_right_press_no_capture(mut self, message: impl OnMouseButton<'a, Message>) -> Self { - self.on_right_press_no_capture = Some(Box::new(message)); + pub fn on_right_press_no_capture(mut self) -> Self { + self.on_right_press_no_capture = true; + self + } + + /// on_right_press will provide window position instead of widget relative + #[must_use] + pub fn on_right_press_window_position(mut self) -> Self { + self.on_right_press_window_position = true; self } @@ -203,8 +211,8 @@ impl<'a, Message, F> OnMouseButton<'a, Message> for F where F: Fn(Option) pub trait OnDrag<'a, Message>: Fn(Option) -> Message + 'a {} impl<'a, Message, F> OnDrag<'a, Message> for F where F: Fn(Option) -> Message + 'a {} -pub trait OnResize<'a, Message>: Fn(Size, Rectangle) -> Message + 'a {} -impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Size, Rectangle) -> Message + 'a {} +pub trait OnResize<'a, Message>: Fn(Rectangle) -> Message + 'a {} +impl<'a, Message, F> OnResize<'a, Message> for F where F: Fn(Rectangle) -> Message + 'a {} pub trait OnScroll<'a, Message>: Fn(mouse::ScrollDelta) -> Option + 'a {} impl<'a, Message, F> OnScroll<'a, Message> for F where @@ -223,7 +231,7 @@ struct State { last_virtual_position: Option, drag_initiated: Option, prev_click: Option<(mouse::Click, Instant)>, - size: Option, + viewport: Option, } impl State { @@ -286,7 +294,8 @@ impl<'a, Message> MouseArea<'a, Message> { on_release: None, on_resize: None, on_right_press: None, - on_right_press_no_capture: None, + on_right_press_no_capture: false, + on_right_press_window_position: false, on_right_release: None, on_middle_press: None, on_middle_release: None, @@ -507,10 +516,9 @@ fn update( let layout_bounds = layout.bounds(); if let Some(message) = widget.on_resize.as_ref() { - let size = layout_bounds.size(); - if state.size != Some(size) { - state.size = Some(size); - shell.publish(message(size, *viewport)); + if state.viewport != Some(*viewport) { + state.viewport = Some(*viewport); + shell.publish(message(*viewport)); } } @@ -635,17 +643,18 @@ fn update( if let Some(message) = widget.on_right_press.as_ref() { if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event { - shell.publish(message(cursor.position_in(layout_bounds))); + let point_opt = if widget.on_right_press_window_position { + cursor.position_over(layout_bounds) + } else { + cursor.position_in(layout_bounds) + }; + shell.publish(message(point_opt)); - return event::Status::Captured; - } - } - - if let Some(message) = widget.on_right_press_no_capture.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event { - shell.publish(message(cursor.position_in(layout_bounds))); - - return event::Status::Ignored; + if widget.on_right_press_no_capture { + return event::Status::Ignored; + } else { + return event::Status::Captured; + } } } diff --git a/src/tab.rs b/src/tab.rs index bc9906d..8d01966 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1508,6 +1508,7 @@ pub enum Command { AddToSidebar(PathBuf), AutoScroll(Option), ChangeLocation(String, Location, Option>), + ContextMenu(Option), Delete(Vec), DropFiles(PathBuf, ClipboardPaste), EmptyTrash, @@ -1566,9 +1567,9 @@ pub enum Message { Reload, RightClick(Option), MiddleClick(usize), + Resize(Rectangle), Scroll(Viewport), ScrollTab(f32), - ScrollToFocus, SearchContext(Location, SearchContextWrapper), SearchReady(bool), SelectAll, @@ -2384,6 +2385,7 @@ pub struct Tab { pub location_context_menu_index: Option, pub context_menu: Option, pub mode: Mode, + pub offset_opt: Option, pub scroll_opt: Option, pub size_opt: Cell>, pub item_view_size_opt: Cell>, @@ -2495,6 +2497,7 @@ impl Tab { location_context_menu_point: None, location_context_menu_index: None, mode: Mode::App, + offset_opt: None, scroll_opt: None, size_opt: Cell::new(None), item_view_size_opt: Cell::new(None), @@ -2861,6 +2864,7 @@ impl Tab { let mut history_i_opt = None; let mod_ctrl = modifiers.contains(Modifiers::CTRL) && self.mode.multiple(); let mod_shift = modifiers.contains(Modifiers::SHIFT) && self.mode.multiple(); + let last_context_menu = self.context_menu; match message { Message::AddNetworkDrive => { commands.push(Command::AddNetworkDrive); @@ -3087,6 +3091,7 @@ impl Tab { self.edit_location = None; if point_opt.is_none() || !mod_shift { self.context_menu = point_opt; + //TODO: hack for clearing selecting when right clicking empty space if self.context_menu.is_some() && self.last_right_click.take().is_none() { if let Some(ref mut items) = self.items_opt { @@ -3578,7 +3583,16 @@ impl Tab { item.highlighted = true; } } + Message::Resize(viewport) => { + self.offset_opt = Some(Vector::new(viewport.x, viewport.y)); + // Scroll to ensure focused item still in view + if let Some(offset) = self.select_focus_scroll() { + commands.push(Command::Iced( + scrollable::scroll_to(self.scrollable_id.clone(), offset).into(), + )); + } + } Message::Scroll(viewport) => { self.scroll_opt = Some(viewport.absolute_offset()); self.watch_drag = true; @@ -3595,13 +3609,6 @@ impl Tab { .into(), )); } - Message::ScrollToFocus => { - if let Some(offset) = self.select_focus_scroll() { - commands.push(Command::Iced( - scrollable::scroll_to(self.scrollable_id.clone(), offset).into(), - )); - } - } Message::SearchContext(location, context) => { if location == self.location { self.search_context = context.0; @@ -3872,6 +3879,18 @@ impl Tab { )); } + //TODO: check for wayland + if self.context_menu != last_context_menu { + if last_context_menu.is_some() { + commands.push(Command::ContextMenu(None)); + } + if let Some(point) = self.context_menu { + commands.push(Command::ContextMenu(Some( + point + self.offset_opt.unwrap_or_default(), + ))); + } + } + // Change directory if requested if let Some(mut location) = cd { if matches!(self.mode, Mode::Desktop) { @@ -4480,9 +4499,9 @@ impl Tab { Message::LocationContextMenuIndex(None) }) } else { - mouse_area = mouse_area.on_right_press_no_capture(move |_point_opt| { - Message::LocationContextMenuIndex(Some(index)) - }) + mouse_area = mouse_area.on_right_press_no_capture().on_right_press( + move |_point_opt| Message::LocationContextMenuIndex(Some(index)), + ) } let mouse_area = if let Location::Path(_) = &self.location { @@ -4739,9 +4758,9 @@ impl Tab { column = column.push(button) } else { column = column.push( - mouse_area::MouseArea::new(button).on_right_press_no_capture( - move |_point_opt| Message::RightClick(Some(i)), - ), + mouse_area::MouseArea::new(button) + .on_right_press_no_capture() + .on_right_press(move |_point_opt| Message::RightClick(Some(i))), ); } } @@ -5158,9 +5177,9 @@ impl Tab { if self.context_menu.is_some() { mouse_area } else { - mouse_area.on_right_press_no_capture(move |_point_opt| { - Message::RightClick(Some(i)) - }) + mouse_area + .on_right_press_no_capture() + .on_right_press(move |_point_opt| Message::RightClick(Some(i))) } }; @@ -5375,8 +5394,7 @@ impl Tab { let mut mouse_area = mouse_area::MouseArea::new(item_view) .on_press(move |_point_opt| Message::Click(None)) .on_release(|_| Message::ClickRelease(None)) - //TODO: better way to keep focused item in view - .on_resize(|_, _| Message::ScrollToFocus) + .on_resize(Message::Resize) .on_back_press(move |_point_opt| Message::GoPrevious) .on_forward_press(move |_point_opt| Message::GoNext) .on_scroll(|delta| respond_to_scroll_direction(delta, self.modifiers)); @@ -5388,12 +5406,13 @@ impl Tab { } let mut popover = widget::popover(mouse_area); - if let Some(point) = self.context_menu { - let context_menu = menu::context_menu(self, key_binds, &self.modifiers); - popover = popover - .popup(context_menu) - .position(widget::popover::Position::Point(point)); + if !cfg!(feature = "wayland") || !crate::is_wayland() { + let context_menu = menu::context_menu(self, key_binds, &self.modifiers); + popover = popover + .popup(context_menu) + .position(widget::popover::Position::Point(point)); + } } let mut tab_column = widget::column::with_capacity(3);