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.
This commit is contained in:
Ian Douglas Scott 2025-01-31 14:17:56 -08:00 committed by Ian Douglas Scott
parent 94ec10686e
commit 3c9a923f41
8 changed files with 585 additions and 95 deletions

16
Cargo.lock generated
View file

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

View file

@ -80,5 +80,8 @@ pub enum Cmd {
ExtWorkspaceHandleV1,
wl_output::WlOutput,
),
MoveWorkspaceBefore(ExtWorkspaceHandleV1, ExtWorkspaceHandleV1),
MoveWorkspaceAfter(ExtWorkspaceHandleV1, ExtWorkspaceHandleV1),
ActivateWorkspace(ExtWorkspaceHandleV1),
SetWorkspacePinned(ExtWorkspaceHandleV1, bool),
}

View file

@ -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();
}
}
}
}
}
}

View file

@ -92,10 +92,36 @@ impl TryFrom<(Vec<u8>, 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<u8>, std::string::String)> for Drag {
type Error = ();
fn try_from((_bytes, mime_type): (Vec<u8>, String)) -> Result<Self, ()> {
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)

View file

@ -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<PathBuf>),
OnScroll(wl_output::WlOutput, ScrollDelta),
TogglePinned(ExtWorkspaceHandleV1),
EnteredWorkspaceSidebarEntry(ExtWorkspaceHandleV1, bool),
Ignore,
}
@ -120,6 +124,8 @@ struct Workspace {
// img_for_output: HashMap<wl_output::WlOutput, backend::CaptureImage>,
img: Option<backend::CaptureImage>,
outputs: HashSet<wl_output::WlOutput>,
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>(

View file

@ -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<D: AsMimeTypes + Send + Clone + 'static>(
drag_content: D,
drag_surface: DragSurface,
id: Option<iced::id::Id>,
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<T>(
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::<HashSet<_>>();
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<Item = &'a Workspace>,
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>(

145
src/widgets/match_size.rs Normal file
View file

@ -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<cosmic::Element<'a, Msg>>,
T2: Into<cosmic::Element<'a, Msg>>,
>(
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<Msg>,
}
impl<Msg> Widget<Msg, cosmic::Theme, cosmic::Renderer> for MatchSize<'_, Msg> {
delegate::delegate! {
to self.matched.as_widget() {
fn size(&self) -> Size<Length>;
fn size_hint(&self) -> Size<Length>;
}
}
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<Tree> {
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<MatchSize<'a, Msg>> for cosmic::Element<'a, Msg> {
fn from(widget: MatchSize<'a, Msg>) -> Self {
cosmic::Element::new(widget)
}
}

View file

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