1
0
Fork 0
forked from wry/wry

Merge pull request #349 from mahkoh/jorth/workspace

ext-workspace: implement v1
This commit is contained in:
mahkoh 2025-01-26 12:39:55 +01:00 committed by GitHub
commit 67549d8cb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 948 additions and 4 deletions

View file

@ -148,6 +148,7 @@ Jay supports the following wayland protocols:
| ext_output_image_capture_source_manager_v1 | 1 | |
| ext_session_lock_manager_v1 | 1 | Yes |
| ext_transient_seat_manager_v1 | 1[^ts_rejected] | Yes |
| ext_workspace_manager_v1 | 1 | Yes |
| jay_tray_v1 | 1 | |
| org_kde_kwin_server_decoration_manager | 1 | |
| wl_compositor | 6 | |

View file

@ -8,6 +8,7 @@
- Add an idle grace period. During the grace period, the screen goes black but is neither
disabled nor locked. This is similar to how android handles going idle. The default is
5 seconds.
- Implement ext-workspace-v1.
# 1.7.0 (2024-10-25)

View file

@ -56,6 +56,7 @@ bitflags! {
CAP_SEAT_MANAGER = 1 << 8,
CAP_DRM_LEASE = 1 << 9,
CAP_INPUT_METHOD = 1 << 10,
CAP_WORKSPACE = 1 << 11,
}
pub const CAPS_DEFAULT: ClientCaps = ClientCaps(CAP_LAYER_SHELL.0 | CAP_DRM_LEASE.0);

View file

@ -27,6 +27,7 @@ use {
xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel, XdgSurface},
WlSurface,
},
workspace_manager::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1,
wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1,
xdg_positioner::XdgPositioner,
@ -39,9 +40,9 @@ use {
},
wire::{
ExtDataControlSourceV1Id, ExtForeignToplevelHandleV1Id, ExtImageCaptureSourceV1Id,
ExtImageCopyCaptureSessionV1Id, JayOutputId, JayScreencastId, JayToplevelId,
JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId, WlPointerId, WlRegionId,
WlRegistryId, WlSeatId, WlSurfaceId, WpDrmLeaseConnectorV1Id,
ExtImageCopyCaptureSessionV1Id, ExtWorkspaceGroupHandleV1Id, JayOutputId,
JayScreencastId, JayToplevelId, JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId,
WlPointerId, WlRegionId, WlRegistryId, WlSeatId, WlSurfaceId, WpDrmLeaseConnectorV1Id,
WpLinuxDrmSyncobjTimelineV1Id, XdgPopupId, XdgPositionerId, XdgSurfaceId,
XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id,
ZwpTabletToolV2Id,
@ -82,6 +83,8 @@ pub struct Objects {
pub ext_copy_sessions:
CopyHashMap<ExtImageCopyCaptureSessionV1Id, Rc<ExtImageCopyCaptureSessionV1>>,
pub ext_data_sources: CopyHashMap<ExtDataControlSourceV1Id, Rc<ExtDataControlSourceV1>>,
pub ext_workspace_groups:
CopyHashMap<ExtWorkspaceGroupHandleV1Id, Rc<ExtWorkspaceGroupHandleV1>>,
ids: RefCell<Vec<usize>>,
}
@ -119,6 +122,7 @@ impl Objects {
foreign_toplevel_handles: Default::default(),
ext_copy_sessions: Default::default(),
ext_data_sources: Default::default(),
ext_workspace_groups: Default::default(),
ids: RefCell::new(vec![]),
}
}
@ -160,6 +164,7 @@ impl Objects {
self.foreign_toplevel_handles.clear();
self.ext_copy_sessions.clear();
self.ext_data_sources.clear();
self.ext_workspace_groups.clear();
}
pub fn id<T>(&self, client_data: &Client) -> Result<T, ClientError>

View file

@ -23,6 +23,7 @@ use {
jay_screencast::{perform_screencast_realloc, perform_toplevel_screencasts},
wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
wl_surface::{zwp_input_popup_surface_v2::input_popup_positioning, NoneSurfaceExt},
workspace_manager::workspace_manager_done,
},
io_uring::{IoUring, IoUringError},
leaks,
@ -278,6 +279,7 @@ fn start_compositor2(
const_40hz_latch: Default::default(),
tray_item_ids: Default::default(),
data_control_device_ids: Default::default(),
workspace_managers: Default::default(),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
@ -446,6 +448,10 @@ fn start_global_event_handlers(
Phase::Present,
handle_const_40hz_latch(state.clone()),
),
eng.spawn(
"workspace manager done",
workspace_manager_done(state.clone()),
),
]
}
@ -593,6 +599,7 @@ fn create_dummy_output(state: &Rc<State>) {
before_latch_event: Default::default(),
tray_start_rel: Default::default(),
tray_items: Default::default(),
ext_workspace_groups: Default::default(),
});
let dummy_workspace = Rc::new(WorkspaceNode {
id: state.node_ids.next(),
@ -615,6 +622,8 @@ fn create_dummy_output(state: &Rc<State>) {
title_texture: Default::default(),
attention_requests: Default::default(),
render_highlight: Default::default(),
ext_workspaces: Default::default(),
opt: Default::default(),
});
*dummy_workspace.output_link.borrow_mut() =
Some(dummy_output.workspaces.add_last(dummy_workspace.clone()));

View file

@ -40,6 +40,7 @@ use {
wl_shm::WlShmGlobal,
wl_subcompositor::WlSubcompositorGlobal,
wl_surface::xwayland_shell_v1::XwaylandShellV1Global,
workspace_manager::ext_workspace_manager_v1::ExtWorkspaceManagerV1Global,
wp_alpha_modifier_v1::WpAlphaModifierV1Global,
wp_commit_timing_manager_v1::WpCommitTimingManagerV1Global,
wp_content_type_manager_v1::WpContentTypeManagerV1Global,
@ -213,6 +214,7 @@ impl Globals {
add_singleton!(WpCommitTimingManagerV1Global);
add_singleton!(ExtDataControlManagerV1Global);
add_singleton!(WlFixesGlobal);
add_singleton!(ExtWorkspaceManagerV1Global);
}
pub fn add_backend_singletons(&self, backend: &Rc<dyn Backend>) {

View file

@ -46,6 +46,7 @@ pub mod wl_shm;
pub mod wl_shm_pool;
pub mod wl_subcompositor;
pub mod wl_surface;
pub mod workspace_manager;
pub mod wp_alpha_modifier_v1;
pub mod wp_commit_timing_manager_v1;
pub mod wp_content_type_manager_v1;

View file

@ -13,7 +13,7 @@ use {
state::{ConnectorData, State},
tree::{calculate_logical_size, OutputNode, TearingMode, VrrMode},
utils::{
cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap,
cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, rc_eq::rc_eq,
transform_ext::TransformExt,
},
wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id},
@ -238,6 +238,11 @@ impl WlOutputGlobal {
if obj.version >= SEND_DONE_SINCE {
obj.send_done();
}
for group in client.objects.ext_workspace_groups.lock().values() {
if rc_eq(&group.output, &self.opt) {
group.handle_new_output(&obj);
}
}
Ok(())
}

View file

@ -0,0 +1,64 @@
use {
crate::{
client::Client,
ifs::workspace_manager::{
ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
ext_workspace_manager_v1::{
ExtWorkspaceManagerV1, WorkspaceManagerId, WorkspaceManagerIds,
},
},
state::State,
tree::{OutputNode, WorkspaceNode},
utils::{copyhashmap::CopyHashMap, opt::Opt, queue::AsyncQueue},
},
std::rc::Rc,
};
pub mod ext_workspace_group_handle_v1;
pub mod ext_workspace_handle_v1;
pub mod ext_workspace_manager_v1;
#[derive(Default)]
pub struct WorkspaceManagerState {
queue: AsyncQueue<Rc<Opt<ExtWorkspaceManagerV1>>>,
dangling_group: Rc<Opt<ExtWorkspaceGroupHandleV1>>,
ids: WorkspaceManagerIds,
managers: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceManagerV1>>,
}
impl WorkspaceManagerState {
pub fn clear(&self) {
self.managers.clear();
self.queue.clear();
}
pub fn announce_output(&self, on: &OutputNode) {
for manager in self.managers.lock().values() {
manager.announce_output(on);
}
}
pub fn announce_workspace(&self, output: &OutputNode, ws: &WorkspaceNode) {
for manager in self.managers.lock().values() {
manager.announce_workspace(output, ws);
}
}
}
pub async fn workspace_manager_done(state: Rc<State>) {
loop {
let manager = state.workspace_managers.queue.pop().await;
if let Some(manager) = manager.get() {
manager.send_done();
}
}
}
fn group_or_dangling(
client: &Client,
group: Option<&ExtWorkspaceGroupHandleV1>,
) -> Rc<Opt<ExtWorkspaceGroupHandleV1>> {
group
.map(|g| g.opt.clone())
.unwrap_or_else(|| client.state.workspace_managers.dangling_group.clone())
}

View file

@ -0,0 +1,164 @@
use {
crate::{
client::{Client, ClientError},
ifs::{
wl_output::{OutputGlobalOpt, WlOutput},
workspace_manager::{
ext_workspace_handle_v1::ExtWorkspaceHandleV1,
ext_workspace_manager_v1::{
ExtWorkspaceManagerV1, WorkspaceChange, WorkspaceManagerId,
},
},
},
leaks::Tracker,
object::{Object, Version},
utils::opt::Opt,
wire::{ext_workspace_group_handle_v1::*, ExtWorkspaceGroupHandleV1Id},
},
std::rc::Rc,
thiserror::Error,
};
pub struct ExtWorkspaceGroupHandleV1 {
pub(super) id: ExtWorkspaceGroupHandleV1Id,
pub(super) client: Rc<Client>,
pub(super) tracker: Tracker<Self>,
pub(super) version: Version,
pub output: Rc<OutputGlobalOpt>,
pub(super) manager_id: WorkspaceManagerId,
pub(super) manager: Rc<Opt<ExtWorkspaceManagerV1>>,
pub(super) opt: Rc<Opt<ExtWorkspaceGroupHandleV1>>,
}
const CAP_CREATE_WORKSPACE: u32 = 1;
impl ExtWorkspaceGroupHandleV1 {
fn detach(&self) {
self.opt.set(None);
if let Some(node) = self.output.node() {
node.ext_workspace_groups.remove(&self.manager_id);
}
}
pub(super) fn send_capabilities(&self) {
let capabilities = CAP_CREATE_WORKSPACE;
self.client.event(Capabilities {
self_id: self.id,
capabilities,
});
}
pub(super) fn send_output_enter(&self, output: &WlOutput) {
self.client.event(OutputEnter {
self_id: self.id,
output: output.id,
});
}
#[expect(dead_code)]
fn send_output_leave(&self, output: &WlOutput) {
self.client.event(OutputLeave {
self_id: self.id,
output: output.id,
});
}
pub(super) fn send_workspace_enter(&self, workspace: &ExtWorkspaceHandleV1) {
self.client.event(WorkspaceEnter {
self_id: self.id,
workspace: workspace.id,
});
}
pub(super) fn send_workspace_leave(&self, workspace: &ExtWorkspaceHandleV1) {
self.client.event(WorkspaceLeave {
self_id: self.id,
workspace: workspace.id,
});
}
fn send_removed(&self) {
self.client.event(Removed { self_id: self.id });
}
pub fn handle_destroyed(&self) {
self.detach();
if let Some(manager) = self.manager.get() {
self.send_removed();
manager.schedule_done();
}
}
pub fn handle_new_output(&self, output: &WlOutput) {
if let Some(manager) = self.manager.get() {
self.send_output_enter(output);
manager.schedule_done();
}
}
}
object_base! {
self = ExtWorkspaceGroupHandleV1;
version = self.version;
}
impl Object for ExtWorkspaceGroupHandleV1 {
fn break_loops(&self) {
self.detach();
}
}
dedicated_add_obj!(
ExtWorkspaceGroupHandleV1,
ExtWorkspaceGroupHandleV1Id,
ext_workspace_groups
);
impl ExtWorkspaceGroupHandleV1RequestHandler for ExtWorkspaceGroupHandleV1 {
type Error = ExtWorkspaceGroupHandleV1Error;
fn create_workspace(
&self,
req: CreateWorkspace<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
if self.opt.is_none() {
return Ok(());
}
let Some(manager) = self.manager.get() else {
return Ok(());
};
manager.pending.push(WorkspaceChange::CreateWorkspace(
req.workspace.to_string(),
self.output.clone(),
));
Ok(())
}
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(manager) = self.manager.get() {
if let Some(node) = self.output.node() {
let mut sent_any = false;
for ws in node.workspaces.iter() {
if let Some(ws) = ws.ext_workspaces.get(&self.manager_id) {
self.send_workspace_leave(&ws);
sent_any = true;
}
}
if sent_any {
manager.schedule_done();
}
}
}
self.detach();
self.client.remove_obj(self)?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum ExtWorkspaceGroupHandleV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ExtWorkspaceGroupHandleV1Error, ClientError);

View file

@ -0,0 +1,215 @@
use {
crate::{
client::{Client, ClientError},
ifs::workspace_manager::{
ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
ext_workspace_manager_v1::{
ExtWorkspaceManagerV1, WorkspaceChange, WorkspaceManagerId,
},
group_or_dangling,
},
leaks::Tracker,
object::{Object, Version},
tree::{OutputNode, WorkspaceNode},
utils::{clonecell::CloneCell, opt::Opt},
wire::{ext_workspace_handle_v1::*, ExtWorkspaceHandleV1Id},
},
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
const STATE_ACTIVE: u32 = 1;
const STATE_URGENT: u32 = 2;
#[expect(dead_code)]
const STATE_HIDDEN: u32 = 4;
const CAP_ACTIVATE: u32 = 1;
#[expect(dead_code)]
const CAP_DEACTIVATE: u32 = 2;
#[expect(dead_code)]
const CAP_REMOVE: u32 = 4;
const CAP_ASSIGN: u32 = 8;
pub struct ExtWorkspaceHandleV1 {
pub(super) id: ExtWorkspaceHandleV1Id,
pub(super) client: Rc<Client>,
pub(super) tracker: Tracker<Self>,
pub version: Version,
pub(super) group: CloneCell<Rc<Opt<ExtWorkspaceGroupHandleV1>>>,
pub(super) workspace: Rc<Opt<WorkspaceNode>>,
pub(super) manager_id: WorkspaceManagerId,
pub(super) manager: Rc<Opt<ExtWorkspaceManagerV1>>,
pub(super) destroyed: Cell<bool>,
}
impl ExtWorkspaceHandleV1 {
fn detach(&self) {
if let Some(ws) = self.workspace.get() {
ws.ext_workspaces.remove(&self.manager_id);
}
}
pub(super) fn send_id(&self, id: &str) {
self.client.event(Id {
self_id: self.id,
id,
});
}
pub(super) fn send_name(&self, name: &str) {
self.client.event(Name {
self_id: self.id,
name,
});
}
#[expect(dead_code)]
fn send_coordinates(&self, coordinates: &[u32]) {
self.client.event(Coordinates {
self_id: self.id,
coordinates,
});
}
pub(super) fn send_current_state(&self) {
let Some(ws) = self.workspace.get() else {
return;
};
let mut state = 0;
let output = ws.output.get();
if let Some(active) = output.workspace.get() {
if active.id == ws.id {
state |= STATE_ACTIVE;
}
}
if ws.attention_requests.active() {
state |= STATE_URGENT;
}
self.send_state(state);
}
fn send_state(&self, state: u32) {
self.client.event(State {
self_id: self.id,
state,
});
}
pub(super) fn send_capabilities(&self) {
let capabilities = CAP_ACTIVATE | CAP_ASSIGN;
self.client.event(Capabilities {
self_id: self.id,
capabilities,
});
}
fn send_removed(&self) {
self.client.event(Removed { self_id: self.id });
}
pub fn handle_destroyed(&self) {
self.destroyed.set(true);
if let Some(manager) = self.manager.get() {
if let Some(group) = self.group.get().get() {
group.send_workspace_leave(self);
}
self.group
.set(self.client.state.workspace_managers.dangling_group.clone());
self.send_state(0);
self.send_removed();
manager.schedule_done();
}
}
pub fn handle_new_output(&self, output: &OutputNode) {
let new = output.ext_workspace_groups.get(&self.manager_id);
let new = group_or_dangling(&self.client, new.as_deref());
let old = self.group.set(new.clone());
if let Some(manager) = self.manager.get() {
if let Some(old) = old.get() {
old.send_workspace_leave(self);
}
if let Some(new) = new.get() {
new.send_workspace_enter(self);
}
manager.schedule_done();
}
}
pub fn handle_visibility_changed(&self) {
if let Some(manager) = self.manager.get() {
self.send_current_state();
manager.schedule_done();
}
}
pub fn handle_urgent_changed(&self) {
self.handle_visibility_changed();
}
}
object_base! {
self = ExtWorkspaceHandleV1;
version = self.version;
}
impl Object for ExtWorkspaceHandleV1 {
fn break_loops(&self) {
self.detach();
}
}
simple_add_obj!(ExtWorkspaceHandleV1);
impl ExtWorkspaceHandleV1RequestHandler for ExtWorkspaceHandleV1 {
type Error = ExtWorkspaceHandleV1Error;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.detach();
self.client.remove_obj(self)?;
Ok(())
}
fn activate(&self, _req: Activate, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if self.destroyed.get() {
return Ok(());
}
let Some(manager) = self.manager.get() else {
return Ok(());
};
manager
.pending
.push(WorkspaceChange::ActivateWorkspace(self.workspace.clone()));
Ok(())
}
fn deactivate(&self, _req: Deactivate, _slf: &Rc<Self>) -> Result<(), Self::Error> {
Ok(())
}
fn assign(&self, req: Assign, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if self.destroyed.get() {
return Ok(());
}
let group = self.client.lookup(req.workspace_group)?;
let Some(manager) = self.manager.get() else {
return Ok(());
};
manager.pending.push(WorkspaceChange::AssignWorkspace(
self.workspace.clone(),
group.output.clone(),
));
Ok(())
}
fn remove(&self, _req: Remove, _slf: &Rc<Self>) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Debug, Error)]
pub enum ExtWorkspaceHandleV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ExtWorkspaceHandleV1Error, ClientError);

View file

@ -0,0 +1,306 @@
use {
crate::{
client::{Client, ClientCaps, ClientError, CAP_WORKSPACE},
globals::{Global, GlobalName},
ifs::{
wl_output::OutputGlobalOpt,
workspace_manager::{
ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
ext_workspace_handle_v1::ExtWorkspaceHandleV1, group_or_dangling,
},
},
leaks::Tracker,
object::{Object, Version},
tree::{move_ws_to_output, OutputNode, WorkspaceNode, WsMoveConfig},
utils::{clonecell::CloneCell, opt::Opt, syncqueue::SyncQueue},
wire::{ext_workspace_manager_v1::*, ExtWorkspaceManagerV1Id},
},
std::{cell::Cell, rc::Rc},
thiserror::Error,
};
linear_ids!(WorkspaceManagerIds, WorkspaceManagerId, u64);
pub struct ExtWorkspaceManagerV1Global {
pub name: GlobalName,
}
pub struct ExtWorkspaceManagerV1 {
id: ExtWorkspaceManagerV1Id,
pub(super) manager_id: WorkspaceManagerId,
client: Rc<Client>,
tracker: Tracker<Self>,
version: Version,
pub(super) pending: SyncQueue<WorkspaceChange>,
opt: Rc<Opt<ExtWorkspaceManagerV1>>,
done_scheduled: Cell<bool>,
}
pub(super) enum WorkspaceChange {
CreateWorkspace(String, Rc<OutputGlobalOpt>),
ActivateWorkspace(Rc<Opt<WorkspaceNode>>),
AssignWorkspace(Rc<Opt<WorkspaceNode>>, Rc<OutputGlobalOpt>),
}
impl ExtWorkspaceManagerV1Global {
pub fn new(name: GlobalName) -> Self {
Self { name }
}
fn bind_(
self: Rc<Self>,
id: ExtWorkspaceManagerV1Id,
client: &Rc<Client>,
version: Version,
) -> Result<(), ExtWorkspaceManagerV1Error> {
let obj = Rc::new(ExtWorkspaceManagerV1 {
id,
manager_id: client.state.workspace_managers.ids.next(),
client: client.clone(),
tracker: Default::default(),
version,
pending: Default::default(),
opt: Default::default(),
done_scheduled: Cell::new(false),
});
track!(client, obj);
client.add_client_obj(&obj)?;
obj.opt.set(Some(obj.clone()));
client
.state
.workspace_managers
.managers
.set(obj.manager_id, obj.clone());
let dummy_output = client.state.dummy_output.get().unwrap();
for ws in dummy_output.workspaces.iter() {
if !ws.is_dummy {
obj.announce_workspace(&dummy_output, &ws);
}
}
for output in client.state.root.outputs.lock().values() {
obj.announce_output(output);
}
Ok(())
}
}
impl ExtWorkspaceManagerV1 {
pub(super) fn announce_output(&self, node: &OutputNode) {
let id = match self.client.new_id() {
Ok(id) => id,
Err(e) => {
self.client.error(e);
return;
}
};
let group = Rc::new(ExtWorkspaceGroupHandleV1 {
id,
client: self.client.clone(),
tracker: Default::default(),
version: self.version,
output: node.global.opt.clone(),
manager_id: self.manager_id,
manager: self.opt.clone(),
opt: Default::default(),
});
track!(self.client, group);
self.client.add_server_obj(&group);
group.opt.set(Some(group.clone()));
node.ext_workspace_groups
.set(self.manager_id, group.clone());
self.send_workspace_group(&group);
group.send_capabilities();
if let Some(bindings) = node.global.bindings.borrow().get(&self.client.id) {
for wl_output in bindings.values() {
group.send_output_enter(wl_output);
}
}
for ws in node.workspaces.iter() {
if let Some(ws) = ws.ext_workspaces.get(&self.manager_id) {
ws.handle_new_output(node);
} else {
self.announce_workspace(node, &ws);
}
}
self.schedule_done();
}
pub(super) fn announce_workspace(&self, output: &OutputNode, workspace: &WorkspaceNode) {
let id = match self.client.new_id() {
Ok(id) => id,
Err(e) => {
self.client.error(e);
return;
}
};
let group = output.ext_workspace_groups.get(&self.manager_id);
let ws = Rc::new(ExtWorkspaceHandleV1 {
id,
client: self.client.clone(),
tracker: Default::default(),
version: self.version,
group: CloneCell::new(group_or_dangling(&self.client, group.as_deref())),
workspace: workspace.opt.clone(),
manager_id: self.manager_id,
manager: self.opt.clone(),
destroyed: Cell::new(false),
});
track!(self.client, ws);
self.client.add_server_obj(&ws);
workspace.ext_workspaces.set(self.manager_id, ws.clone());
self.send_workspace(&ws);
ws.send_capabilities();
ws.send_id(&workspace.name);
ws.send_name(&workspace.name);
ws.send_current_state();
if let Some(group) = group {
group.send_workspace_enter(&ws);
}
self.schedule_done();
}
fn send_workspace_group(&self, workspace_group: &ExtWorkspaceGroupHandleV1) {
self.client.event(WorkspaceGroup {
self_id: self.id,
workspace_group: workspace_group.id,
});
}
fn send_workspace(&self, workspace: &ExtWorkspaceHandleV1) {
self.client.event(Workspace {
self_id: self.id,
workspace: workspace.id,
});
}
pub(super) fn send_done(&self) {
self.done_scheduled.set(false);
self.client.event(Done { self_id: self.id });
}
fn send_finished(&self) {
self.client.event(Finished { self_id: self.id });
}
fn detach(&self) {
self.opt.set(None);
self.pending.clear();
self.client
.state
.workspace_managers
.managers
.remove(&self.manager_id);
}
pub(super) fn schedule_done(&self) {
if self.done_scheduled.replace(true) {
return;
}
self.client
.state
.workspace_managers
.queue
.push(self.opt.clone());
}
}
global_base!(
ExtWorkspaceManagerV1Global,
ExtWorkspaceManagerV1,
ExtWorkspaceManagerV1Error
);
impl Global for ExtWorkspaceManagerV1Global {
fn singleton(&self) -> bool {
true
}
fn version(&self) -> u32 {
1
}
fn required_caps(&self) -> ClientCaps {
CAP_WORKSPACE
}
}
simple_add_global!(ExtWorkspaceManagerV1Global);
object_base! {
self = ExtWorkspaceManagerV1;
version = self.version;
}
impl Object for ExtWorkspaceManagerV1 {
fn break_loops(&self) {
self.detach();
}
}
simple_add_obj!(ExtWorkspaceManagerV1);
impl ExtWorkspaceManagerV1RequestHandler for ExtWorkspaceManagerV1 {
type Error = ExtWorkspaceManagerV1Error;
fn commit(&self, _req: Commit, _slf: &Rc<Self>) -> Result<(), Self::Error> {
while let Some(change) = self.pending.pop() {
match change {
WorkspaceChange::ActivateWorkspace(w) => {
let Some(ws) = w.get() else {
continue;
};
let output = ws.output.get();
output.show_workspace(&ws);
ws.flush_jay_workspaces();
output.schedule_update_render_data();
self.client.state.tree_changed();
}
WorkspaceChange::AssignWorkspace(w, o) => {
let Some(ws) = w.get() else {
continue;
};
let Some(o) = o.node() else {
continue;
};
let link = match &*ws.output_link.borrow() {
None => continue,
Some(l) => l.to_ref(),
};
let config = WsMoveConfig {
make_visible_always: false,
make_visible_if_empty: true,
source_is_destroyed: false,
before: None,
};
move_ws_to_output(&link, &o, config);
ws.desired_output.set(o.global.output_id.clone());
self.client.state.tree_changed();
}
WorkspaceChange::CreateWorkspace(name, output) => {
if self.client.state.workspaces.contains(&name) {
return Ok(());
}
let Some(output) = output.node() else {
return Ok(());
};
output.create_workspace(&name);
}
}
}
Ok(())
}
fn stop(&self, _req: Stop, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.detach();
self.send_finished();
self.client.remove_obj(self)?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum ExtWorkspaceManagerV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
}
efrom!(ExtWorkspaceManagerV1Error, ClientError);

View file

@ -57,6 +57,7 @@ use {
zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2,
NoneSurfaceExt,
},
workspace_manager::WorkspaceManagerState,
wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1,
wp_drm_lease_device_v1::WpDrmLeaseDeviceV1Global,
wp_linux_drm_syncobj_manager_v1::WpLinuxDrmSyncobjManagerV1Global,
@ -229,6 +230,7 @@ pub struct State {
pub const_40hz_latch: EventSource<dyn LatchListener>,
pub tray_item_ids: TrayItemIds,
pub data_control_device_ids: DataControlDeviceIds,
pub workspace_managers: WorkspaceManagerState,
}
// impl Drop for State {
@ -917,6 +919,7 @@ impl State {
self.ei_clients.clear();
self.slow_ei_clients.clear();
self.toplevels.clear();
self.workspace_managers.clear();
}
pub fn damage_hardware_cursors(&self, render: bool) {

View file

@ -197,6 +197,7 @@ impl ConnectorHandler {
before_latch_event: Default::default(),
tray_start_rel: Default::default(),
tray_items: Default::default(),
ext_workspace_groups: Default::default(),
});
on.update_visible();
on.update_rects();
@ -259,6 +260,7 @@ impl ConnectorHandler {
self.state.add_global(&tray);
self.state.tree_changed();
on.update_presentation_type();
self.state.workspace_managers.announce_output(&on);
'outer: loop {
while let Some(event) = self.data.connector.event() {
match event {
@ -331,6 +333,9 @@ impl ConnectorHandler {
};
move_ws_to_output(&ws, &target, config);
}
for group in on.ext_workspace_groups.lock().drain_values() {
group.handle_destroyed();
}
for seat in self.state.globals.seats.lock().values() {
seat.cursor_group().output_disconnected(&on, &target);
}

View file

@ -23,6 +23,10 @@ use {
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
},
workspace_manager::{
ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
ext_workspace_manager_v1::WorkspaceManagerId,
},
wp_content_type_v1::ContentType,
zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP},
zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
@ -97,6 +101,7 @@ pub struct OutputNode {
pub before_latch_event: EventSource<dyn BeforeLatchListener>,
pub tray_start_rel: Cell<i32>,
pub tray_items: LinkedList<Rc<dyn DynTrayItem>>,
pub ext_workspace_groups: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceGroupHandleV1>>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
@ -414,6 +419,7 @@ impl OutputNode {
self.screencasts.clear();
self.screencopies.clear();
self.ext_copy_sessions.clear();
self.ext_workspace_groups.clear();
}
pub fn on_spaces_changed(self: &Rc<Self>) {
@ -635,6 +641,9 @@ impl OutputNode {
jw.send_destroyed();
jw.workspace.set(None);
}
for wh in old.ext_workspaces.lock().values() {
wh.handle_destroyed();
}
old.clear();
self.state.workspaces.remove(&old.name);
} else {
@ -678,7 +687,10 @@ impl OutputNode {
title_texture: Default::default(),
attention_requests: Default::default(),
render_highlight: Default::default(),
ext_workspaces: Default::default(),
opt: Default::default(),
});
ws.opt.set(Some(ws.clone()));
ws.update_has_captures();
*ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone()));
self.state.workspaces.set(name.to_string(), ws.clone());
@ -694,6 +706,7 @@ impl OutputNode {
for (client, e) in clients_to_kill.values() {
client.error(e);
}
self.state.workspace_managers.announce_workspace(self, &ws);
self.schedule_update_render_data();
ws
}

View file

@ -10,6 +10,10 @@ use {
wl_surface::{
x_surface::xwindow::Xwindow, xdg_surface::xdg_toplevel::XdgToplevel, WlSurface,
},
workspace_manager::{
ext_workspace_handle_v1::ExtWorkspaceHandleV1,
ext_workspace_manager_v1::WorkspaceManagerId,
},
},
rect::Rect,
renderer::Renderer,
@ -25,6 +29,7 @@ use {
copyhashmap::CopyHashMap,
linkedlist::{LinkedList, LinkedNode, NodeRef},
numcell::NumCell,
opt::Opt,
threshold_counter::ThresholdCounter,
},
wire::JayWorkspaceId,
@ -60,6 +65,8 @@ pub struct WorkspaceNode {
pub title_texture: RefCell<Option<TextTexture>>,
pub attention_requests: ThresholdCounter,
pub render_highlight: NumCell<u32>,
pub ext_workspaces: CopyHashMap<WorkspaceManagerId, Rc<ExtWorkspaceHandleV1>>,
pub opt: Rc<Opt<WorkspaceNode>>,
}
impl WorkspaceNode {
@ -68,6 +75,8 @@ impl WorkspaceNode {
*self.output_link.borrow_mut() = None;
self.fullscreen.set(None);
self.jay_workspaces.clear();
self.ext_workspaces.clear();
self.opt.set(None);
}
pub fn update_has_captures(&self) {
@ -95,6 +104,9 @@ impl WorkspaceNode {
pub fn set_output(&self, output: &Rc<OutputNode>) {
self.output.set(output.clone());
for wh in self.ext_workspaces.lock().values() {
wh.handle_new_output(output);
}
for jw in self.jay_workspaces.lock().values() {
jw.send_output(output);
}
@ -171,6 +183,9 @@ impl WorkspaceNode {
for jw in self.jay_workspaces.lock().values() {
jw.send_visible(visible);
}
for wh in self.ext_workspaces.lock().values() {
wh.handle_visibility_changed();
}
for stacked in self.stacked.iter() {
stacked.stacked_prepare_set_visible();
}
@ -236,6 +251,9 @@ impl WorkspaceNode {
fn mod_attention_requested(&self, set: bool) {
let crossed_threshold = self.attention_requests.adj(set);
if crossed_threshold {
for wh in self.ext_workspaces.lock().values() {
wh.handle_urgent_changed();
}
self.output.get().schedule_update_render_data();
}
}

View file

@ -35,6 +35,7 @@ pub mod on_drop_event;
pub mod once;
pub mod opaque;
pub mod opaque_cell;
pub mod opt;
pub mod option_ext;
pub mod oserror;
pub mod page_size;

27
src/utils/opt.rs Normal file
View file

@ -0,0 +1,27 @@
use {crate::utils::clonecell::CloneCell, std::rc::Rc};
pub struct Opt<T> {
t: CloneCell<Option<Rc<T>>>,
}
impl<T> Default for Opt<T> {
fn default() -> Self {
Self {
t: Default::default(),
}
}
}
impl<T> Opt<T> {
pub fn set(&self, t: Option<Rc<T>>) {
self.t.set(t);
}
pub fn get(&self) -> Option<Rc<T>> {
self.t.get()
}
pub fn is_none(&self) -> bool {
self.t.is_none()
}
}

View file

@ -53,4 +53,10 @@ impl<T> SyncQueue<T> {
self.swap(&mut res);
res
}
pub fn clear(&self) {
unsafe {
self.el.get().deref_mut().clear();
}
}
}

View file

@ -0,0 +1,31 @@
event capabilities {
capabilities: u32,
}
event output_enter {
output: id(wl_output),
}
event output_leave {
output: id(wl_output),
}
event workspace_enter {
workspace: id(ext_workspace_handle_v1),
}
event workspace_leave {
workspace: id(ext_workspace_handle_v1),
}
event removed {
}
request create_workspace {
workspace: str,
}
request destroy {
}

View file

@ -0,0 +1,43 @@
event id {
id: str,
}
event name {
name: str,
}
event coordinates {
coordinates: array(u32),
}
event state {
state: u32,
}
event capabilities {
capabilities: u32,
}
event removed {
}
request destroy {
}
request activate {
}
request deactivate {
}
request assign {
workspace_group: id(ext_workspace_group_handle_v1),
}
request remove {
}

View file

@ -0,0 +1,23 @@
event workspace_group {
workspace_group: id(ext_workspace_group_handle_v1),
}
event workspace {
workspace: id(ext_workspace_handle_v1),
}
request commit {
}
event done {
}
event finished {
}
request stop {
}