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)]