From 3c9a923f4194209c70c1ce5a5998d83751762301 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 31 Jan 2025 14:17:56 -0800 Subject: [PATCH] Add support for workspace pinning and dragging Workspaces can be pinned, and dragged to reorder or move to a different output. These features are enabled only if cosmic-workspace-v2 advertises the necessary protocol version and capabilities. The layout of the labels and pin buttons could be tweaked a bit still. Some hacks and workarounds are needed to get drag and drop working as desired. Something iced and libcosmic could potentially improve in the future. But this now seems fairly robust. --- Cargo.lock | 16 +- src/backend/mod.rs | 3 + src/backend/wayland/mod.rs | 57 +++++++ src/dnd.rs | 30 ++++ src/main.rs | 89 +++++++++- src/view/mod.rs | 338 ++++++++++++++++++++++++++++--------- src/widgets/match_size.rs | 145 ++++++++++++++++ src/widgets/mod.rs | 2 + 8 files changed, 585 insertions(+), 95 deletions(-) create mode 100644 src/widgets/match_size.rs diff --git a/Cargo.lock b/Cargo.lock index 9252a4e..0e067d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1087,7 +1087,7 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#6b05c2a157118979cb472a38455ba78ca9729196" +source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#bc4af9183e0967802d7fbe91ba811a29ca6a3b67" dependencies = [ "bitflags 2.8.0", "cosmic-protocols", @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#6b05c2a157118979cb472a38455ba78ca9729196" +source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#bc4af9183e0967802d7fbe91ba811a29ca6a3b67" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -1672,7 +1672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3185,7 +3185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -4539,7 +4539,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4552,7 +4552,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5000,7 +5000,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix 0.38.44", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5844,7 +5844,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/src/backend/mod.rs b/src/backend/mod.rs index c0e4a2d..805c967 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -80,5 +80,8 @@ pub enum Cmd { ExtWorkspaceHandleV1, wl_output::WlOutput, ), + MoveWorkspaceBefore(ExtWorkspaceHandleV1, ExtWorkspaceHandleV1), + MoveWorkspaceAfter(ExtWorkspaceHandleV1, ExtWorkspaceHandleV1), ActivateWorkspace(ExtWorkspaceHandleV1), + SetWorkspacePinned(ExtWorkspaceHandleV1, bool), } diff --git a/src/backend/wayland/mod.rs b/src/backend/wayland/mod.rs index 769035d..32debec 100644 --- a/src/backend/wayland/mod.rs +++ b/src/backend/wayland/mod.rs @@ -3,6 +3,7 @@ use calloop_wayland_source::WaylandSource; use cctk::{ + cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2, screencopy::{CaptureSource, ScreencopyState}, sctk::{ self, @@ -106,12 +107,68 @@ impl AppData { } } } + // TODO version check + Cmd::MoveWorkspaceBefore(workspace_handle, other_workspace_handle) => { + if let Ok(workspace_manager) = self.workspace_state.workspace_manager().get() { + if let Some(cosmic_workspace) = self + .workspace_state + .workspaces() + .find(|w| w.handle == workspace_handle) + .and_then(|w| w.cosmic_handle.as_ref()) + { + if cosmic_workspace.version() + >= zcosmic_workspace_handle_v2::REQ_MOVE_BEFORE_SINCE + { + cosmic_workspace.move_before(&other_workspace_handle, 0); + workspace_manager.commit(); + } + } + } + } + Cmd::MoveWorkspaceAfter(workspace_handle, other_workspace_handle) => { + if let Ok(workspace_manager) = self.workspace_state.workspace_manager().get() { + if let Some(cosmic_workspace) = self + .workspace_state + .workspaces() + .find(|w| w.handle == workspace_handle) + .and_then(|w| w.cosmic_handle.as_ref()) + { + if cosmic_workspace.version() + >= zcosmic_workspace_handle_v2::REQ_MOVE_AFTER_SINCE + { + cosmic_workspace.move_after(&other_workspace_handle, 0); + workspace_manager.commit(); + } + } + } + } Cmd::ActivateWorkspace(workspace_handle) => { if let Ok(workspace_manager) = self.workspace_state.workspace_manager().get() { workspace_handle.activate(); workspace_manager.commit(); } } + Cmd::SetWorkspacePinned(workspace_handle, pinned) => { + if let Ok(workspace_manager) = self.workspace_state.workspace_manager().get() { + if let Some(cosmic_workspace) = self + .workspace_state + .workspaces() + .find(|w| w.handle == workspace_handle) + .and_then(|w| w.cosmic_handle.as_ref()) + { + if cosmic_workspace.version() >= zcosmic_workspace_handle_v2::REQ_PIN_SINCE + { + // TODO check capability + if pinned { + cosmic_workspace.pin(); + } else { + cosmic_workspace.unpin(); + } + workspace_manager.commit(); + } + } + } + } } } diff --git a/src/dnd.rs b/src/dnd.rs index d1ff402..c321582 100644 --- a/src/dnd.rs +++ b/src/dnd.rs @@ -92,10 +92,36 @@ impl TryFrom<(Vec, std::string::String)> for DragWorkspace { } } +// TODO name? +pub enum Drag { + Toplevel, + Workspace, +} + +impl cosmic::iced::clipboard::mime::AllowedMimeTypes for Drag { + fn allowed() -> Cow<'static, [String]> { + vec![TOPLEVEL_MIME.clone(), WORKSPACE_MIME.clone()].into() + } +} + +impl TryFrom<(Vec, std::string::String)> for Drag { + type Error = (); + fn try_from((_bytes, mime_type): (Vec, String)) -> Result { + if mime_type == *TOPLEVEL_MIME { + Ok(Self::Toplevel) + } else if mime_type == *WORKSPACE_MIME { + Ok(Self::Workspace) + } else { + Err(()) + } + } +} + #[derive(Clone, Debug, PartialEq)] #[repr(u8)] pub enum DropTarget { WorkspaceSidebarEntry(ExtWorkspaceHandleV1, wl_output::WlOutput), + WorkspaceSidebarDragPlaceholder(ExtWorkspaceHandleV1, wl_output::WlOutput), OutputToplevels(ExtWorkspaceHandleV1, wl_output::WlOutput), #[allow(dead_code)] WorkspacesBar(wl_output::WlOutput), @@ -112,6 +138,10 @@ impl DropTarget { let id = workspace.id().protocol_id(); (u64::from(discriminant) << 32) | u64::from(id) } + Self::WorkspaceSidebarDragPlaceholder(workspace, _output) => { + let id = workspace.id().protocol_id(); + (u64::from(discriminant) << 32) | u64::from(id) + } Self::OutputToplevels(_workspace, output) => { let id = output.id().protocol_id(); (u64::from(discriminant) << 32) | u64::from(id) diff --git a/src/main.rs b/src/main.rs index 5b445af..82fb348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use cctk::{ cosmic_protocols::toplevel_management::v1::client::zcosmic_toplevel_manager_v1, + cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2, sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}, wayland_client::{protocol::wl_output, Connection, Proxy}, wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1, @@ -15,6 +16,7 @@ use cosmic::{ cctk, dbus_activation, iced::{ self, + clipboard::dnd::{DndEvent, SourceEvent}, event::wayland::{Event as WaylandEvent, LayerEvent, OutputEvent}, keyboard::key::{Key, Named}, mouse::ScrollDelta, @@ -111,6 +113,8 @@ enum Msg { BgConfig(cosmic_bg_config::state::State), UpdateToplevelIcon(String, Option), OnScroll(wl_output::WlOutput, ScrollDelta), + TogglePinned(ExtWorkspaceHandleV1), + EnteredWorkspaceSidebarEntry(ExtWorkspaceHandleV1, bool), Ignore, } @@ -120,6 +124,8 @@ struct Workspace { // img_for_output: HashMap, img: Option, outputs: HashSet, + has_cursor: bool, + dnd_source_id: iced::id::Id, } impl Workspace { @@ -132,6 +138,12 @@ impl Workspace { .state .contains(ext_workspace_handle_v1::State::Active) } + + fn is_pinned(&self) -> bool { + self.info + .cosmic_state + .contains(zcosmic_workspace_handle_v2::State::Pinned) + } } #[derive(Clone, Debug)] @@ -396,17 +408,20 @@ impl Application for App { self.workspaces = Vec::new(); for (outputs, workspace) in workspaces { // XXX efficiency - #[allow(clippy::mutable_key_type)] - let img = old_workspaces + let old_workspace = old_workspaces .iter() - .find(|i| *i.handle() == workspace.handle) - .map(|i| i.img.clone()) - .unwrap_or_default(); + .find(|i| *i.handle() == workspace.handle); + let img = old_workspace.map(|i| i.img.clone()).unwrap_or_default(); + let has_cursor = old_workspace.is_some_and(|w| w.has_cursor); + let dnd_source_id = old_workspace + .map_or_else(iced::id::Id::unique, |w| w.dnd_source_id.clone()); self.workspaces.push(Workspace { info: workspace, outputs, img, + has_cursor, + dnd_source_id, }); } self.update_capture_filter(); @@ -530,7 +545,11 @@ impl Application for App { output, )); } - Some(DropTarget::WorkspacesBar(_)) | None => {} + Some( + DropTarget::WorkspacesBar(_) + | DropTarget::WorkspaceSidebarDragPlaceholder(_, _), + ) + | None => {} } } } @@ -626,7 +645,58 @@ impl Application for App { } } Msg::DndWorkspaceDrag => {} - Msg::DndWorkspaceDrop(_workspace) => {} + Msg::DndWorkspaceDrop(_workspace) => { + if let Some((DragSurface::Workspace(handle), _)) = &self.drag_surface { + match self.drop_target.take() { + Some( + DropTarget::WorkspaceSidebarEntry(other_handle, _output) + | DropTarget::WorkspaceSidebarDragPlaceholder(other_handle, _output), + ) => { + let workspace = self.workspaces.iter().find(|i| i.handle() == handle); + let other_workspace = + self.workspaces.iter().find(|i| *i.handle() == other_handle); + if let (Some(workspace), Some(other_workspace)) = + (workspace, other_workspace) + { + if workspace.outputs == other_workspace.outputs + && workspace.info.coordinates[0] + 1 + == other_workspace.info.coordinates[0] + { + // Workspace is already in requested position + } else { + self.send_wayland_cmd(backend::Cmd::MoveWorkspaceBefore( + handle.clone(), + other_handle, + )); + } + } + } + Some(DropTarget::OutputToplevels(_, _) | DropTarget::WorkspacesBar(_)) + | None => {} + } + } + } + Msg::TogglePinned(workspace_handle) => { + if let Some(workspace) = self + .workspaces + .iter() + .find(|w| *w.handle() == workspace_handle) + { + self.send_wayland_cmd(backend::Cmd::SetWorkspacePinned( + workspace_handle, + !workspace.is_pinned(), + )); + } + } + Msg::EnteredWorkspaceSidebarEntry(workspace_handle, entered) => { + if let Some(workspace) = self + .workspaces + .iter_mut() + .find(|w| *w.handle() == workspace_handle) + { + workspace.has_cursor = entered; + } + } Msg::Ignore => {} } @@ -656,6 +726,11 @@ impl Application for App { modified_key: _, physical_key: _, }) => Some(Msg::Close), + // XXX Workaround for `on_finish`/`on_cancel` not being called, seemingly + // due to state diffing behavior. + iced::Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { + Some(Msg::SourceFinished) + } _ => None, }); let config_subscription = cosmic_config::config_subscription::<_, CosmicWorkspacesConfig>( diff --git a/src/view/mod.rs b/src/view/mod.rs index e61766e..7743a16 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,29 +1,59 @@ use cosmic::{ cctk::{ - cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1, + cosmic_protocols::{ + toplevel_info::v1::client::zcosmic_toplevel_handle_v1, + workspace::v2::client::zcosmic_workspace_handle_v2, + }, wayland_client::protocol::wl_output, wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1, }, iced::{ self, advanced::layout::flex::Axis, - clipboard::mime::AllowedMimeTypes, + clipboard::mime::{AllowedMimeTypes, AsMimeTypes}, widget::{column, row}, Border, Length, }, iced_core::{text::Wrapping, Shadow}, iced_winit::platform_specific::wayland::subsurface_widget::Subsurface, - widget, Apply, + widget::{self, Widget}, + Apply, }; use cosmic_comp_config::workspace::WorkspaceLayout; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::{ backend::{self, CaptureImage}, - dnd::{DragSurface, DragToplevel, DragWorkspace, DropTarget}, + dnd::{Drag, DragSurface, DragToplevel, DragWorkspace, DropTarget}, App, LayerSurface, Msg, Toplevel, Workspace, }; +fn dnd_source_with_drag_surface( + drag_content: D, + drag_surface: DragSurface, + id: Option, + child: cosmic::Element<'_, Msg>, + drag_icon: impl Fn() -> cosmic::Element<'static, Msg> + 'static, +) -> cosmic::Element<'_, Msg> { + let mut source = cosmic::widget::dnd_source(child) + .drag_threshold(5.) + .drag_content(move || drag_content.clone()) + .drag_icon(move |offset| { + ( + drag_icon().map(|_| ()), + cosmic::iced_core::widget::tree::State::None, + -offset, + ) + }) + .on_start(Some(Msg::StartDrag(drag_surface))) + .on_finish(Some(Msg::SourceFinished)) + .on_cancel(Some(Msg::SourceFinished)); + if let Some(id) = id { + source.set_id(id); + } + source.into() +} + fn dnd_destination_for_target( target: DropTarget, child: cosmic::Element<'_, Msg>, @@ -50,22 +80,31 @@ pub(crate) fn layer_surface<'a>( app: &'a App, surface: &'a LayerSurface, ) -> cosmic::Element<'a, Msg> { - let mut drop_target = None; - if let Some(DropTarget::WorkspaceSidebarEntry(workspace, output)) = &app.drop_target { - if output == &surface.output { - drop_target = Some(workspace); - } - } let mut drag_toplevel = None; - if let Some((DragSurface::Toplevel(handle), _)) = &app.drag_surface { - drag_toplevel = Some(handle); + let mut drag_workspace = None; + match &app.drag_surface { + Some((DragSurface::Toplevel(handle), _)) => { + drag_toplevel = Some(handle); + } + Some((DragSurface::Workspace(handle), _)) => { + drag_workspace = Some(handle); + } + _ => {} } + #[allow(clippy::mutable_key_type)] + let workspaces_with_toplevels = app + .toplevels + .iter() + .flat_map(|t| &t.info.workspace) + .collect::>(); let layout = app.conf.workspace_config.workspace_layout; let sidebar = workspaces_sidebar( app.workspaces_for_output(&surface.output), + &workspaces_with_toplevels, &surface.output, layout, - drop_target, + app.drop_target.as_ref(), + drag_workspace, ); let toplevels = toplevel_previews( app.toplevels.iter().filter(|i| { @@ -122,6 +161,57 @@ fn close_button(on_press: Msg) -> cosmic::Element<'static, Msg> { .into() } +fn pin_button_style(theme: &cosmic::Theme, is_pinned: bool) -> cosmic::widget::button::Style { + let bg_color = if is_pinned { + theme.cosmic().accent.base.into() + } else { + theme.cosmic().primary.base.into() + }; + let icon_color = if is_pinned { + theme.cosmic().accent.on.into() + } else { + theme.cosmic().primary.on.into() + }; + cosmic::widget::button::Style { + icon_color: Some(icon_color), + background: Some(iced::Background::Color(bg_color)), + border_radius: theme.cosmic().corner_radii.radius_s.into(), + ..cosmic::widget::button::Style::new() + } +} + +fn pin_button(workspace: &Workspace) -> cosmic::Element<'static, Msg> { + let is_pinned = workspace.is_pinned(); + crate::widgets::visibility_wrapper( + widget::button::custom( + widget::icon::from_name("pin-symbolic") + .symbolic(true) + .size(16), //.style(|theme, status| todo!()) + ) + //.class(cosmic::theme::Button::Icon) + //.class(cosmic::theme::Button::Image) + .class(cosmic::theme::Button::Custom { + // TODO adjust state for hover, etc. + active: Box::new(move |_, theme| pin_button_style(theme, is_pinned)), + disabled: Box::new(move |theme| pin_button_style(theme, is_pinned)), + hovered: Box::new(move |_, theme| pin_button_style(theme, is_pinned)), + pressed: Box::new(move |_, theme| pin_button_style(theme, is_pinned)), + }) + //.class(cosmic::theme::Button::Standard) + // TODO style selected correctly + .selected(workspace.is_pinned()) + .on_press(Msg::TogglePinned(workspace.handle().clone())), + // Show pin button only if hovered or pinned; but allocate space the same way + // regardless + (workspace.has_cursor || workspace.is_pinned()) + && workspace + .info + .cosmic_capabilities + .contains(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Pin), + ) + .into() +} + fn workspace_item_appearance( theme: &cosmic::Theme, is_active: bool, @@ -149,8 +239,9 @@ fn workspace_item( _output: &wl_output::WlOutput, layout: WorkspaceLayout, is_drop_target: bool, + has_workspace_drag: bool, ) -> cosmic::Element<'static, Msg> { - let (image, image_height) = if let Some(img) = workspace.img.as_ref() { + let (mut image, image_height) = if let Some(img) = workspace.img.as_ref() { let is_rotated = matches!( img.transform, wl_output::Transform::_90 @@ -188,28 +279,38 @@ fn workspace_item( ) }; - let workspace_name = widget::text::body(fl!( - "workspace", - HashMap::from([("number", &workspace.info.name)]) - )); + let workspace_name = row![ + widget::text::body(fl!( + "workspace", + HashMap::from([("number", &workspace.info.name)]) + )) + .width(Length::Fill) // XXX mades workspace bar fill screen + .align_x(iced::Alignment::Center), + pin_button(workspace), + ]; // Needed to prevent text getting pushed out when scaling on Vertical layout - let content = match layout { - WorkspaceLayout::Horizontal => column![image, workspace_name] - .align_x(iced::Alignment::Center) - .spacing(4) - .apply(widget::container), - WorkspaceLayout::Vertical => column![image.height(Length::Fill), workspace_name] - .align_x(iced::Alignment::Center) - .spacing(4) - .apply(widget::container) - .max_height(image_height + 21.0 + 4.0), // text height + spacing - }; + if layout == WorkspaceLayout::Vertical { + image = image.height(Length::Fill); + } + let mut content = crate::widgets::size_cross_nth( + vec![ + image.into(), + iced::widget::Space::with_height(4.0).into(), + workspace_name.into(), + ], + Axis::Vertical, + 0, // Size container to match image size + ) + .apply(widget::container); + if layout == WorkspaceLayout::Vertical { + content = content.max_height(image_height + 21.0 + 4.0); // text height + spacing + } - let is_active = workspace.is_active(); + let is_active = workspace.is_active() && !has_workspace_drag; // TODO editable name? let mut button = widget::button::custom(content) - .selected(workspace.is_active()) + .selected(is_active) .class(cosmic::theme::Button::Custom { active: Box::new(move |_focused, theme| { workspace_item_appearance(theme, is_active, is_drop_target) @@ -235,11 +336,37 @@ fn workspace_item( button.into() } +fn workspace_drag_placeholder( + other_workspace: &Workspace, + other_output: &wl_output::WlOutput, + layout: WorkspaceLayout, +) -> cosmic::Element<'static, Msg> { + let drop_target = DropTarget::WorkspaceSidebarDragPlaceholder( + other_workspace.handle().clone(), + other_output.clone(), + ); + let placeholder = widget::button::custom(widget::Space::new(Length::Fill, Length::Fill)) + .class(cosmic::theme::Button::Custom { + active: Box::new(|_, _| unreachable!()), + disabled: Box::new(|theme| workspace_item_appearance(theme, true, true)), + hovered: Box::new(|_, _| unreachable!()), + pressed: Box::new(|_, _| unreachable!()), + }) + .padding(8); + let placeholder = crate::widgets::match_size( + workspace_item(other_workspace, other_output, layout, true, true), + placeholder, + ); + dnd_destination_for_target(drop_target, placeholder.into(), Msg::DndWorkspaceDrop) +} + fn workspace_sidebar_entry<'a>( workspace: &'a Workspace, output: &'a wl_output::WlOutput, layout: WorkspaceLayout, is_drop_target: bool, + has_toplevels: bool, + has_workspace_drag: bool, ) -> cosmic::Element<'a, Msg> { /* XXX let mouse_interaction = if is_drop_target { @@ -248,45 +375,107 @@ fn workspace_sidebar_entry<'a>( iced::mouse::Interaction::Idle }; */ - let item = workspace_item(workspace, output, layout, is_drop_target); - /* TODO allow moving workspaces (needs compositor support) + let item = workspace_item( + workspace, + output, + layout, + is_drop_target, + has_workspace_drag, + ); + let item = iced::widget::mouse_area(item) + .on_enter(Msg::EnteredWorkspaceSidebarEntry( + workspace.handle().clone(), + true, + )) + .on_exit(Msg::EnteredWorkspaceSidebarEntry( + workspace.handle().clone(), + false, + )); let workspace_clone = workspace.clone(); // TODO avoid clone let output_clone = output.clone(); - let source = cosmic::widget::dnd_source(item) - .drag_threshold(5.) - .drag_content(|| DragWorkspace {}) - .drag_icon(move |offset| { - ( - workspace_item(&workspace_clone, &output_clone, false).map(|_| ()), - cosmic::iced_core::widget::tree::State::None, - -offset, - ) - }) - .on_start(Some(Msg::StartDrag(DragSurface::Workspace( - workspace.handle.clone(), - )))) - .on_finish(Some(Msg::SourceFinished)) - .on_cancel(Some(Msg::SourceFinished)) - .into(); - */ - //crate::widgets::mouse_interaction_wrapper( - // mouse_interaction, - dnd_destination_for_target( - DropTarget::WorkspaceSidebarEntry(workspace.handle().clone(), output.clone()), - item, - Msg::DndToplevelDrop, - ) + let drop_target = DropTarget::WorkspaceSidebarEntry(workspace.handle().clone(), output.clone()); + let destination = + dnd_destination_for_target(drop_target, item.into(), |drag: Drag| match drag { + Drag::Toplevel => Msg::DndToplevelDrop(DragToplevel {}), + Drag::Workspace => Msg::DndWorkspaceDrop(DragWorkspace {}), + }); + // Cosmic-comp auto-removes workspaces that aren't pinned and don't have toplevels when they + // aren't the last workspace. So it shouldn't be possible to drag. + if (has_toplevels || workspace.is_pinned()) + && workspace + .info + .cosmic_capabilities + .contains(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Move) + { + dnd_source_with_drag_surface( + DragWorkspace {}, + DragSurface::Workspace(workspace.handle().clone()), + Some(workspace.dnd_source_id.clone()), + destination, + move || workspace_item(&workspace_clone, &output_clone, layout, false, true), + ) + } else { + destination + } } +#[allow(clippy::mutable_key_type)] fn workspaces_sidebar<'a>( workspaces: impl Iterator, + workspaces_with_toplevels: &HashSet<&backend::ExtWorkspaceHandleV1>, output: &'a wl_output::WlOutput, layout: WorkspaceLayout, - drop_target: Option<&backend::ExtWorkspaceHandleV1>, + drop_target: Option<&DropTarget>, + drag_workspace: Option<&'a backend::ExtWorkspaceHandleV1>, ) -> cosmic::Element<'a, Msg> { - let sidebar_entries = workspaces - .map(|w| workspace_sidebar_entry(w, output, layout, drop_target == Some(w.handle()))) - .collect(); + let mut sidebar_entries = Vec::new(); + for workspace in workspaces { + // XXX Need dnd source with same id for drag to work; but give it 0x0 size + if drag_workspace == Some(workspace.handle()) { + let workspace_clone = workspace.clone(); + let output_clone = output.clone(); + let source = dnd_source_with_drag_surface( + DragWorkspace {}, + DragSurface::Workspace(workspace.handle().clone()), + Some(workspace.dnd_source_id.clone()), + widget::Space::new(Length::Shrink, Length::Shrink).into(), + move || workspace_item(&workspace_clone, &output_clone, layout, false, true), + ); + sidebar_entries.push(source); + continue; + } + + let mut drop_target_is_workspace = false; + let mut drop_target_is_placeholder = false; + match drop_target { + Some(DropTarget::WorkspaceSidebarEntry(w, o)) + if (w, o) == (workspace.handle(), output) => + { + drop_target_is_workspace = true; + } + Some(DropTarget::WorkspaceSidebarDragPlaceholder(w, o)) + if (w, o) == (workspace.handle(), output) => + { + drop_target_is_placeholder = true; + } + _ => {} + } + + if drag_workspace.is_some() + && drag_workspace != Some(workspace.handle()) + && (drop_target_is_workspace || drop_target_is_placeholder) + { + sidebar_entries.push(workspace_drag_placeholder(workspace, output, layout)); + } + sidebar_entries.push(workspace_sidebar_entry( + workspace, + output, + layout, + drop_target_is_workspace && drag_workspace.is_none(), + workspaces_with_toplevels.contains(workspace.handle()), + drag_workspace.is_some(), + )); + } let (axis, width, height) = match layout { WorkspaceLayout::Vertical => (Axis::Vertical, Length::Shrink, Length::Fill), WorkspaceLayout::Horizontal => (Axis::Horizontal, Length::Fill, Length::Shrink), @@ -396,24 +585,13 @@ fn toplevel_previews_entry( !is_being_dragged, ); let toplevel2 = toplevel.clone(); - cosmic::widget::dnd_source::<_, DragToplevel>(preview) - .drag_threshold(5.) - .drag_content(|| DragToplevel {}) - // XXX State? - .drag_icon(move |offset| { - ( - toplevel_preview(&toplevel2, true).map(|_| ()), - cosmic::iced_core::widget::tree::State::None, - -offset, - ) - }) - .on_start(Some(Msg::StartDrag( - //size, - DragSurface::Toplevel(toplevel.handle.clone()), - ))) - .on_finish(Some(Msg::SourceFinished)) - .on_cancel(Some(Msg::SourceFinished)) - .into() + dnd_source_with_drag_surface( + DragToplevel {}, + DragSurface::Toplevel(toplevel.handle.clone()), + None, + preview.into(), + move || toplevel_preview(&toplevel2, true), + ) } fn toplevel_previews<'a>( diff --git a/src/widgets/match_size.rs b/src/widgets/match_size.rs new file mode 100644 index 0000000..e746cda --- /dev/null +++ b/src/widgets/match_size.rs @@ -0,0 +1,145 @@ +//! Show one surface, sized to match the size of another (invisible) widget + +use cosmic::iced::{ + advanced::{ + layout, mouse, renderer, + widget::{Operation, Tree}, + Clipboard, Layout, Shell, Widget, + }, + event::{self, Event}, + Length, Rectangle, Size, +}; +use std::marker::PhantomData; + +pub fn match_size< + 'a, + Msg, + T1: Into>, + T2: Into>, +>( + matched: T1, + shown: T2, +) -> MatchSize<'a, Msg> { + MatchSize { + matched: matched.into(), + shown: shown.into(), + _msg: PhantomData, + } +} + +pub struct MatchSize<'a, Msg> { + matched: cosmic::Element<'a, Msg>, + shown: cosmic::Element<'a, Msg>, + _msg: PhantomData, +} + +impl Widget for MatchSize<'_, Msg> { + delegate::delegate! { + to self.matched.as_widget() { + fn size(&self) -> Size; + fn size_hint(&self) -> Size; + } + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &cosmic::Renderer, + operation: &mut dyn Operation<()>, + ) { + self.matched + .as_widget() + .operate(&mut tree.children[0], layout, renderer, operation); + self.shown + .as_widget() + .operate(&mut tree.children[1], layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &cosmic::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Msg>, + viewport: &Rectangle, + ) -> event::Status { + self.shown.as_widget_mut().on_event( + &mut tree.children[1], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &cosmic::Renderer, + ) -> mouse::Interaction { + self.shown.as_widget().mouse_interaction( + &tree.children[1], + layout, + cursor, + viewport, + renderer, + ) + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &cosmic::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + // TODO? + self.matched + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut cosmic::Renderer, + theme: &cosmic::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.shown.as_widget().draw( + &tree.children[1], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.matched), Tree::new(&self.shown)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut [&mut self.matched, &mut self.shown]); + } +} + +impl<'a, Msg: 'a> From> for cosmic::Element<'a, Msg> { + fn from(widget: MatchSize<'a, Msg>) -> Self { + cosmic::Element::new(widget) + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b14ef14..667b60b 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -19,6 +19,8 @@ mod toplevels; pub use toplevels::toplevels; mod visibility_wrapper; pub use visibility_wrapper::visibility_wrapper; +mod match_size; +pub use match_size::match_size; // Widget for debugging #[allow(dead_code)]