Improve multi-output support
059c81bfb6872ca99fd021dcfe50c396debe5ebd
1 parent
517b92d4
README
+5 -4
| 101 | 101 | ||
| 102 | 102 | ||
| 103 | 103 | # BARS AND STATUS |
|
| 104 | 104 | ||
| 105 | 105 | swm has no built-in bar. A layer-shell bar such as Waybar[2] can use the |
|
| 106 | - | `ext/workspaces` module, backed by swm's ext-workspace-v1 implementation. |
|
| 107 | - | Empty inactive workspaces are advertised as hidden; occupied and selected |
|
| 108 | - | workspaces remain visible. See `swmctl(1)` for workspace metadata and Waybar |
|
| 109 | - | integration. |
|
| 106 | + | `ext/workspaces` module, backed by swm's ext-workspace-v1 implementation. Each |
|
| 107 | + | output has its own workspace group, so the active button identifies the |
|
| 108 | + | workspace shown on that output. Empty inactive workspaces are advertised as |
|
| 109 | + | hidden; occupied workspaces remain visible. See `swmctl(1)` for workspace |
|
| 110 | + | metadata and Waybar integration. |
|
| 110 | 111 | ||
| 111 | 112 | On each state change swm also writes line-oriented status records to standard |
|
| 112 | 113 | output. When `-s` is used, that output is connected to the startup command's |
|
| 113 | 114 | standard input. Workspace metadata uses `workspace N title VALUE` and |
|
| 114 | 115 | `workspace N color #RRGGBBAA` records; empty values mean unset. The active window |
swm.c
+183 -72
| 72 | 72 | #include <wlr/types/wlr_xdg_activation_v1.h> |
|
| 73 | 73 | #include <wlr/types/wlr_xdg_decoration_v1.h> |
|
| 74 | 74 | #include <wlr/types/wlr_xdg_dialog_v1.h> |
|
| 75 | 75 | #include <wlr/types/wlr_xdg_output_v1.h> |
|
| 76 | 76 | #include <wlr/types/wlr_xdg_shell.h> |
|
| 77 | - | #include <wlr/types/wlr_xdg_toplevel_icon_v1.h> |
|
| 78 | 77 | #include <wlr/types/wlr_xdg_system_bell_v1.h> |
|
| 78 | + | #include <wlr/types/wlr_xdg_toplevel_icon_v1.h> |
|
| 79 | 79 | #include <wlr/util/log.h> |
|
| 80 | 80 | #include <wlr/util/region.h> |
|
| 81 | 81 | #include <wlr/xwayland.h> |
|
| 82 | 82 | #include <xcb/xcb.h> |
|
| 83 | 83 | #include <xcb/xcb_icccm.h> |
| 113 | 113 | #define MAX_WINDOW_STATE_FIELD 256 |
|
| 114 | 114 | #define MAX_STATUS_FIELD 1024 |
|
| 115 | 115 | #define MAX_WINDOW_STATE_LINE 1024 |
|
| 116 | 116 | #define MAX_STATE_PATH 4096 |
|
| 117 | 117 | #define MAX_WORKSPACE_TITLE 256 |
|
| 118 | - | #define MAX_WS_ID 16 |
|
| 118 | + | #define MAX_WS_ID 256 |
|
| 119 | 119 | #define MAX_STATIC_LISTENERS 128 |
|
| 120 | 120 | #define MAX_GLOBAL_LISTENERS 64 |
|
| 121 | 121 | #define MAX_WS_MANAGERS 64 |
|
| 122 | - | #define MAX_WS_HANDLES (MAX_WS_MANAGERS * WSCOUNT) |
|
| 122 | + | #define MAX_WS_GROUPS (MAX_WS_MANAGERS * MAX_MONITORS) |
|
| 123 | + | #define MAX_WS_HANDLES (MAX_WS_GROUPS * WSCOUNT) |
|
| 123 | 124 | #define MAX_AUTOSTART 64 |
|
| 124 | 125 | #define MAX_COMMAND_SIZE 1024 |
|
| 125 | 126 | #define MAX_COMMAND_ARGS 64 |
|
| 126 | 127 | ||
| 127 | 128 | /* Cursor interaction state. */ |
| 183 | 184 | ||
| 184 | 185 | /* Display and its window-manager state. */ |
|
| 185 | 186 | typedef struct monitor_t monitor_t; |
|
| 186 | 187 | /* Global workspace state. */ |
|
| 187 | 188 | typedef struct workspace_t workspace_t; |
|
| 189 | + | /* Per-output workspace group exported to one protocol client. */ |
|
| 190 | + | typedef struct workspace_group_t workspace_group_t; |
|
| 188 | 191 | ||
| 189 | 192 | /* Managed XDG or Xwayland client. */ |
|
| 190 | 193 | typedef struct { |
|
| 191 | 194 | /* This must remain first so generic surface code can read it. */ |
|
| 192 | 195 | unsigned int type; /* XDG_SHELL or X11. */ |
| 343 | 346 | const layout_t *prevlt; |
|
| 344 | 347 | stack_state_t v, h; |
|
| 345 | 348 | struct wl_list handles; /* Protocol handles that publish this workspace. */ |
|
| 346 | 349 | }; |
|
| 347 | 350 | ||
| 348 | - | /* Each connected workspace client gets one manager and one group for all displays. */ |
|
| 349 | 351 | /* Bound ext-workspace manager. */ |
|
| 350 | 352 | typedef struct { |
|
| 351 | - | struct wl_resource *resource; /* Client's workspace manager. */ |
|
| 352 | - | struct wl_resource *group; /* Client's group, or nullptr after it disconnects. */ |
|
| 353 | - | workspace_t *pending; /* Workspace to activate when the client commits. */ |
|
| 354 | - | struct wl_list link; /* Entry in ws_managers. */ |
|
| 353 | + | struct wl_resource *resource; /* Client's workspace manager. */ |
|
| 354 | + | workspace_t *pending; /* Workspace to activate when the client commits. */ |
|
| 355 | + | monitor_t *pending_monitor; /* Target output for the pending activation. */ |
|
| 356 | + | struct wl_list groups; /* workspace_group_t.link */ |
|
| 357 | + | struct wl_list link; /* Entry in ws_managers. */ |
|
| 355 | 358 | } workspace_manager_t; |
|
| 356 | 359 | ||
| 360 | + | /* Per-output workspace group exported to one protocol client. */ |
|
| 361 | + | struct workspace_group_t { |
|
| 362 | + | struct wl_resource *resource; |
|
| 363 | + | workspace_manager_t *mgr; |
|
| 364 | + | monitor_t *mon; |
|
| 365 | + | struct wl_list handles; /* workspace_handle_t.group_link */ |
|
| 366 | + | struct wl_list link; /* Entry in workspace_manager_t.groups. */ |
|
| 367 | + | }; |
|
| 368 | + | ||
| 357 | 369 | /* Exported ext-workspace handle. */ |
|
| 358 | 370 | typedef struct { |
|
| 359 | 371 | struct wl_resource *resource; |
|
| 360 | 372 | workspace_t *ws; |
|
| 361 | - | workspace_manager_t *mgr; /* nullptr after the manager is destroyed. */ |
|
| 362 | - | struct wl_list link; /* Entry in workspace_t.handles. */ |
|
| 373 | + | workspace_manager_t *mgr; /* nullptr after the manager is destroyed. */ |
|
| 374 | + | workspace_group_t *group; /* nullptr after the group is destroyed. */ |
|
| 375 | + | struct wl_list link; /* Entry in workspace_t.handles. */ |
|
| 376 | + | struct wl_list group_link; /* Entry in workspace_group_t.handles. */ |
|
| 363 | 377 | } workspace_handle_t; |
|
| 364 | 378 | ||
| 365 | 379 | /* Bound client for swm's ephemeral workspace metadata protocol. */ |
|
| 366 | 380 | typedef struct { |
|
| 367 | 381 | struct wl_resource *resource; |
| 623 | 637 | ); |
|
| 624 | 638 | static void toplevel_control_bind( |
|
| 625 | 639 | struct wl_client *client, void *data, uint32_t version, uint32_t id |
|
| 626 | 640 | ); |
|
| 627 | 641 | static void workspace_metadata_broadcast(void); |
|
| 628 | - | static void workspace_handle_create(workspace_manager_t *mgr, workspace_t *ws); |
|
| 642 | + | static void workspace_group_create(workspace_manager_t *mgr, monitor_t *m); |
|
| 643 | + | static void workspace_handle_create( |
|
| 644 | + | workspace_manager_t *mgr, workspace_group_t *group, workspace_t *ws |
|
| 645 | + | ); |
|
| 629 | 646 | static void workspace_manager_bind( |
|
| 630 | 647 | struct wl_client *client, void *data, uint32_t version, uint32_t id |
|
| 631 | 648 | ); |
|
| 649 | + | static void workspace_monitor_create(monitor_t *m); |
|
| 650 | + | static void workspace_monitor_destroy(monitor_t *m); |
|
| 632 | 651 | static void workspace_output_bind(struct wl_listener *listener, void *data); |
|
| 633 | - | static uint32_t workspace_state(workspace_t *ws); |
|
| 652 | + | static uint32_t workspace_state(workspace_t *ws, monitor_t *m); |
|
| 634 | 653 | static void set_primary_selection(struct wl_listener *listener, void *data); |
|
| 635 | 654 | static void set_selection(struct wl_listener *listener, void *data); |
|
| 636 | 655 | static void setup(void); |
|
| 637 | 656 | static void spawn(const arg_t *arg); |
|
| 638 | 657 | static void expand_argv(const char *const *argv, char **expanded, char *storage); |
| 872 | 891 | POOL(session_lock, session_lock_t, MAX_SESSION_LOCKS); |
|
| 873 | 892 | POOL(pending_spawn, pending_spawn_t, MAX_PENDING_SPAWNS); |
|
| 874 | 893 | POOL(window_state, window_state_t, MAX_WINDOW_STATES); |
|
| 875 | 894 | POOL(static_listener, static_listener_t, MAX_STATIC_LISTENERS); |
|
| 876 | 895 | POOL(workspace_manager, workspace_manager_t, MAX_WS_MANAGERS); |
|
| 896 | + | POOL(workspace_group, workspace_group_t, MAX_WS_GROUPS); |
|
| 877 | 897 | POOL(workspace_handle, workspace_handle_t, MAX_WS_HANDLES); |
|
| 878 | 898 | POOL(metadata_manager, metadata_manager_t, MAX_WS_MANAGERS); |
|
| 879 | 899 | ||
| 880 | 900 | /* Backend-specific client operations. */ |
|
| 881 | 901 | /* Client helpers shared by the XDG shell and Xwayland implementations. These |
| 2193 | 2213 | wlr_layer_surface_v1_destroy(l->layer_surface); |
|
| 2194 | 2214 | } |
|
| 2195 | 2215 | wl_list_remove(&m->destroy.link); |
|
| 2196 | 2216 | wl_list_remove(&m->frame.link); |
|
| 2197 | 2217 | wl_list_remove(&m->link); |
|
| 2218 | + | workspace_monitor_destroy(m); |
|
| 2198 | 2219 | wl_list_remove(&m->request_state.link); |
|
| 2199 | 2220 | wl_list_remove(&m->output_bind.link); |
|
| 2200 | 2221 | ||
| 2201 | 2222 | if (m->lock_surface) |
|
| 2202 | 2223 | destroy_lock_surface(&m->destroy_lock_surface, nullptr); |
| 2700 | 2721 | LISTEN(&wlr_output->events.destroy, &m->destroy, cleanup_monitor); |
|
| 2701 | 2722 | LISTEN(&wlr_output->events.request_state, &m->request_state, request_monitor_state); |
|
| 2702 | 2723 | LISTEN(&wlr_output->events.bind, &m->output_bind, workspace_output_bind); |
|
| 2703 | 2724 | ||
| 2704 | 2725 | wl_list_insert(&mons, &m->link); |
|
| 2726 | + | workspace_monitor_create(m); |
|
| 2705 | 2727 | print_status(); |
|
| 2706 | 2728 | ||
| 2707 | 2729 | /* Hide unrelated content behind a transparent fullscreen window, as the |
|
| 2708 | 2730 | * XDG shell protocol requires. */ |
|
| 2709 | 2731 | /* update_monitors() will set the final size and position. */ |
| 5698 | 5720 | wl_resource_set_implementation(resource, &toplevel_control_impl, nullptr, nullptr); |
|
| 5699 | 5721 | } |
|
| 5700 | 5722 | ||
| 5701 | 5723 | /* |
|
| 5702 | 5724 | * ext-workspace-v1 implementation. wlroots 0.19 ships no helper for this |
|
| 5703 | - | * protocol, so the resources are managed by hand. swm advertises a single |
|
| 5704 | - | * workspace group spanning all outputs, containing every workspace; the |
|
| 5705 | - | * only capability is activate, which behaves like a workspace switch. |
|
| 5725 | + | * protocol, so the resources are managed by hand. Each output has a workspace |
|
| 5726 | + | * group containing handles for every global workspace. State is relative to |
|
| 5727 | + | * the group's output so a bar can identify that output's active workspace. |
|
| 5706 | 5728 | */ |
|
| 5707 | 5729 | static void workspace_manager_commit(struct wl_client *client, struct wl_resource *resource) { |
|
| 5708 | 5730 | workspace_manager_t *mgr = wl_resource_get_user_data(resource); |
|
| 5709 | 5731 | workspace_t *ws; |
|
| 5732 | + | monitor_t *m; |
|
| 5710 | 5733 | ||
| 5711 | - | if (!mgr || !(ws = mgr->pending)) |
|
| 5734 | + | if (!mgr || !(ws = mgr->pending) || !(m = mgr->pending_monitor)) |
|
| 5712 | 5735 | return; |
|
| 5713 | - | mgr->pending = nullptr; |
|
| 5714 | - | view_workspace(ws, selmon); |
|
| 5736 | + | mgr->pending = nullptr; |
|
| 5737 | + | mgr->pending_monitor = nullptr; |
|
| 5738 | + | view_workspace(ws, m); |
|
| 5715 | 5739 | } |
|
| 5716 | 5740 | ||
| 5717 | 5741 | /* Stop publishing workspaces to a manager. */ |
|
| 5718 | 5742 | static void workspace_manager_stop(struct wl_client *client, struct wl_resource *resource) { |
|
| 5719 | 5743 | ext_workspace_manager_v1_send_finished(resource); |
| 5726 | 5750 | }; |
|
| 5727 | 5751 | ||
| 5728 | 5752 | /* Stop publishing workspaces after a manager client disconnects. */ |
|
| 5729 | 5753 | static void workspace_manager_destroy(struct wl_resource *resource) { |
|
| 5730 | 5754 | workspace_manager_t *mgr = wl_resource_get_user_data(resource); |
|
| 5731 | - | workspace_handle_t *h; |
|
| 5755 | + | workspace_group_t *group, *group_tmp; |
|
| 5756 | + | workspace_handle_t *h, *h_tmp; |
|
| 5732 | 5757 | int i; |
|
| 5733 | 5758 | ||
| 5734 | 5759 | if (!mgr) |
|
| 5735 | 5760 | return; |
|
| 5736 | - | /* Handles and the group may outlive the manager resource. */ |
|
| 5761 | + | /* Child resources may outlive the manager resource. */ |
|
| 5737 | 5762 | for (i = 0; i < WSCOUNT; i++) { |
|
| 5738 | 5763 | wl_list_for_each(h, &workspaces[i].handles, link) { |
|
| 5739 | 5764 | if (h->mgr == mgr) |
|
| 5740 | 5765 | h->mgr = nullptr; |
|
| 5741 | 5766 | } |
|
| 5742 | 5767 | } |
|
| 5743 | - | if (mgr->group) |
|
| 5744 | - | wl_resource_set_user_data(mgr->group, nullptr); |
|
| 5768 | + | wl_list_for_each_safe(group, group_tmp, &mgr->groups, link) { |
|
| 5769 | + | wl_list_for_each_safe(h, h_tmp, &group->handles, group_link) { |
|
| 5770 | + | h->group = nullptr; |
|
| 5771 | + | wl_list_remove(&h->group_link); |
|
| 5772 | + | } |
|
| 5773 | + | wl_resource_set_user_data(group->resource, nullptr); |
|
| 5774 | + | wl_list_remove(&group->link); |
|
| 5775 | + | pool_release(&workspace_group_pool, group); |
|
| 5776 | + | } |
|
| 5745 | 5777 | wl_list_remove(&mgr->link); |
|
| 5746 | 5778 | pool_release(&workspace_manager_pool, mgr); |
|
| 5747 | 5779 | } |
|
| 5748 | 5780 | ||
| 5749 | 5781 | /* Reject a client-created workspace. */ |
| 5751 | 5783 | struct wl_client *client, struct wl_resource *resource, const char *name |
|
| 5752 | 5784 | ) { |
|
| 5753 | 5785 | /* Ignore creation because swm has a fixed set of workspaces. */ |
|
| 5754 | 5786 | } |
|
| 5755 | 5787 | ||
| 5756 | - | /* Reject destruction of the global workspace group. */ |
|
| 5788 | + | /* Destroy a client's workspace group handle. */ |
|
| 5757 | 5789 | static void workspace_group_destroy_request( |
|
| 5758 | 5790 | struct wl_client *client, struct wl_resource *resource |
|
| 5759 | 5791 | ) { |
|
| 5760 | 5792 | wl_resource_destroy(resource); |
|
| 5761 | 5793 | } |
| 5763 | 5795 | static const struct ext_workspace_group_handle_v1_interface ws_group_impl = { |
|
| 5764 | 5796 | .create_workspace = workspace_group_create_workspace, |
|
| 5765 | 5797 | .destroy = workspace_group_destroy_request, |
|
| 5766 | 5798 | }; |
|
| 5767 | 5799 | ||
| 5768 | - | /* Remove a disconnected client's global workspace group. */ |
|
| 5800 | + | /* Remove a group destroyed by its client. */ |
|
| 5769 | 5801 | static void workspace_group_destroy(struct wl_resource *resource) { |
|
| 5770 | - | workspace_manager_t *mgr = wl_resource_get_user_data(resource); |
|
| 5802 | + | workspace_group_t *group = wl_resource_get_user_data(resource); |
|
| 5803 | + | workspace_handle_t *h, *tmp; |
|
| 5771 | 5804 | ||
| 5772 | - | if (mgr) |
|
| 5773 | - | mgr->group = nullptr; |
|
| 5805 | + | if (!group) |
|
| 5806 | + | return; |
|
| 5807 | + | wl_list_for_each_safe(h, tmp, &group->handles, group_link) { |
|
| 5808 | + | h->group = nullptr; |
|
| 5809 | + | wl_list_remove(&h->group_link); |
|
| 5810 | + | } |
|
| 5811 | + | wl_list_remove(&group->link); |
|
| 5812 | + | pool_release(&workspace_group_pool, group); |
|
| 5774 | 5813 | } |
|
| 5775 | 5814 | ||
| 5776 | - | /* Queue workspace activation. */ |
|
| 5815 | + | /* Queue workspace activation on the output represented by its group. */ |
|
| 5777 | 5816 | static void workspace_activate(struct wl_client *client, struct wl_resource *resource) { |
|
| 5778 | 5817 | workspace_handle_t *h = wl_resource_get_user_data(resource); |
|
| 5779 | 5818 | ||
| 5780 | - | if (h && h->mgr) |
|
| 5781 | - | h->mgr->pending = h->ws; |
|
| 5819 | + | if (h && h->mgr && h->group) { |
|
| 5820 | + | h->mgr->pending = h->ws; |
|
| 5821 | + | h->mgr->pending_monitor = h->group->mon; |
|
| 5822 | + | } |
|
| 5782 | 5823 | } |
|
| 5783 | 5824 | ||
| 5784 | 5825 | /* Ignore workspace deactivation. */ |
|
| 5785 | 5826 | static void workspace_deactivate(struct wl_client *client, struct wl_resource *resource) { |
|
| 5786 | 5827 | /* Ignore deactivation because swm always has an active workspace. */ |
| 5788 | 5829 | ||
| 5789 | 5830 | /* Ignore client workspace assignment. */ |
|
| 5790 | 5831 | static void workspace_assign( |
|
| 5791 | 5832 | struct wl_client *client, struct wl_resource *resource, struct wl_resource *group |
|
| 5792 | 5833 | ) { |
|
| 5793 | - | /* Ignore assignment because swm has only one workspace group. */ |
|
| 5834 | + | /* Ignore assignment because output groups contain fixed workspace handles. */ |
|
| 5794 | 5835 | } |
|
| 5795 | 5836 | ||
| 5796 | 5837 | /* Ignore client workspace removal. */ |
|
| 5797 | 5838 | static void workspace_remove_request(struct wl_client *client, struct wl_resource *resource) { |
|
| 5798 | 5839 | /* Ignore removal because swm has a fixed set of workspaces. */ |
| 5815 | 5856 | static void workspace_handle_destroy(struct wl_resource *resource) { |
|
| 5816 | 5857 | workspace_handle_t *h = wl_resource_get_user_data(resource); |
|
| 5817 | 5858 | ||
| 5818 | 5859 | if (!h) |
|
| 5819 | 5860 | return; |
|
| 5861 | + | if (h->group) |
|
| 5862 | + | wl_list_remove(&h->group_link); |
|
| 5820 | 5863 | wl_list_remove(&h->link); |
|
| 5821 | 5864 | pool_release(&workspace_handle_pool, h); |
|
| 5822 | 5865 | } |
|
| 5823 | 5866 | ||
| 5824 | - | /* Publish one workspace to a connected workspace manager. */ |
|
| 5825 | - | void workspace_handle_create(workspace_manager_t *mgr, workspace_t *ws) { |
|
| 5867 | + | /* Publish one workspace in a connected client's output group. */ |
|
| 5868 | + | void workspace_handle_create(workspace_manager_t *mgr, workspace_group_t *group, workspace_t *ws) { |
|
| 5826 | 5869 | workspace_handle_t *h = pool_take(&workspace_handle_pool); |
|
| 5827 | 5870 | uint32_t coord; |
|
| 5828 | 5871 | struct wl_array coords = { sizeof(coord), sizeof(coord), &coord }; |
|
| 5829 | 5872 | char id[MAX_WS_ID]; |
|
| 5873 | + | int id_len; |
|
| 5830 | 5874 | ||
| 5831 | 5875 | if (!h) { |
|
| 5832 | 5876 | wl_client_post_no_memory(wl_resource_get_client(mgr->resource)); |
|
| 5833 | 5877 | return; |
|
| 5834 | 5878 | } |
|
| 5835 | 5879 | h->ws = ws; |
|
| 5836 | 5880 | h->mgr = mgr; |
|
| 5881 | + | h->group = group; |
|
| 5837 | 5882 | h->resource = wl_resource_create( |
|
| 5838 | 5883 | wl_resource_get_client(mgr->resource), &ext_workspace_handle_v1_interface, 1, 0 |
|
| 5839 | 5884 | ); |
|
| 5840 | 5885 | ||
| 5841 | 5886 | if (!h->resource) { |
| 5843 | 5888 | wl_client_post_no_memory(wl_resource_get_client(mgr->resource)); |
|
| 5844 | 5889 | return; |
|
| 5845 | 5890 | } |
|
| 5846 | 5891 | wl_resource_set_implementation(h->resource, &ws_handle_impl, h, workspace_handle_destroy); |
|
| 5847 | 5892 | wl_list_insert(&ws->handles, &h->link); |
|
| 5893 | + | wl_list_insert(&group->handles, &h->group_link); |
|
| 5848 | 5894 | ||
| 5849 | 5895 | ext_workspace_manager_v1_send_workspace(mgr->resource, h->resource); |
|
| 5850 | - | snprintf(id, sizeof(id), "ws%d", ws->idx + 1); |
|
| 5851 | - | ext_workspace_handle_v1_send_id(h->resource, id); |
|
| 5896 | + | id_len = snprintf(id, sizeof(id), "%s:ws%d", group->mon->wlr_output->name, ws->idx + 1); |
|
| 5897 | + | if (id_len > 0 && (size_t)id_len < sizeof(id)) |
|
| 5898 | + | ext_workspace_handle_v1_send_id(h->resource, id); |
|
| 5852 | 5899 | ext_workspace_handle_v1_send_name(h->resource, ws->name); |
|
| 5853 | 5900 | coord = (uint32_t)ws->idx; |
|
| 5854 | 5901 | ext_workspace_handle_v1_send_coordinates(h->resource, &coords); |
|
| 5855 | 5902 | ext_workspace_handle_v1_send_capabilities( |
|
| 5856 | 5903 | h->resource, EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ACTIVATE |
|
| 5857 | 5904 | ); |
|
| 5858 | - | ext_workspace_handle_v1_send_state(h->resource, workspace_state(ws)); |
|
| 5905 | + | ext_workspace_handle_v1_send_state(h->resource, workspace_state(ws, group->mon)); |
|
| 5906 | + | ext_workspace_group_handle_v1_send_workspace_enter(group->resource, h->resource); |
|
| 5907 | + | } |
|
| 5859 | 5908 | ||
| 5860 | - | if (mgr->group) |
|
| 5861 | - | ext_workspace_group_handle_v1_send_workspace_enter(mgr->group, h->resource); |
|
| 5909 | + | /* Publish one output's workspace group to a connected client. */ |
|
| 5910 | + | void workspace_group_create(workspace_manager_t *mgr, monitor_t *m) { |
|
| 5911 | + | workspace_group_t *group = pool_take(&workspace_group_pool); |
|
| 5912 | + | struct wl_resource *out; |
|
| 5913 | + | struct wl_client *client = wl_resource_get_client(mgr->resource); |
|
| 5914 | + | int i; |
|
| 5915 | + | ||
| 5916 | + | if (!group) { |
|
| 5917 | + | wl_client_post_no_memory(client); |
|
| 5918 | + | return; |
|
| 5919 | + | } |
|
| 5920 | + | group->mgr = mgr; |
|
| 5921 | + | group->mon = m; |
|
| 5922 | + | group->resource = wl_resource_create(client, &ext_workspace_group_handle_v1_interface, 1, 0); |
|
| 5923 | + | if (!group->resource) { |
|
| 5924 | + | pool_release(&workspace_group_pool, group); |
|
| 5925 | + | wl_client_post_no_memory(client); |
|
| 5926 | + | return; |
|
| 5927 | + | } |
|
| 5928 | + | wl_list_init(&group->handles); |
|
| 5929 | + | wl_list_insert(&mgr->groups, &group->link); |
|
| 5930 | + | wl_resource_set_implementation(group->resource, &ws_group_impl, group, workspace_group_destroy); |
|
| 5931 | + | ext_workspace_manager_v1_send_workspace_group(mgr->resource, group->resource); |
|
| 5932 | + | ext_workspace_group_handle_v1_send_capabilities(group->resource, 0); |
|
| 5933 | + | wl_resource_for_each(out, &m->wlr_output->resources) { |
|
| 5934 | + | if (wl_resource_get_client(out) == client) |
|
| 5935 | + | ext_workspace_group_handle_v1_send_output_enter(group->resource, out); |
|
| 5936 | + | } |
|
| 5937 | + | for (i = 0; i < WSCOUNT; i++) |
|
| 5938 | + | workspace_handle_create(mgr, group, &workspaces[i]); |
|
| 5862 | 5939 | } |
|
| 5863 | 5940 | ||
| 5864 | - | /* Publish swm's workspace group to a newly connected client. */ |
|
| 5941 | + | /* Publish output groups to a newly connected workspace client. */ |
|
| 5865 | 5942 | void workspace_manager_bind(struct wl_client *client, void *data, uint32_t version, uint32_t id) { |
|
| 5866 | 5943 | workspace_manager_t *mgr = pool_take(&workspace_manager_pool); |
|
| 5867 | 5944 | monitor_t *m; |
|
| 5868 | - | struct wl_resource *out; |
|
| 5869 | - | int i; |
|
| 5870 | 5945 | ||
| 5871 | 5946 | if (!mgr) { |
|
| 5872 | 5947 | wl_client_post_no_memory(client); |
|
| 5873 | 5948 | return; |
|
| 5874 | 5949 | } |
|
| 5875 | 5950 | mgr->resource = |
|
| 5876 | 5951 | wl_resource_create(client, &ext_workspace_manager_v1_interface, (int)version, id); |
|
| 5877 | - | ||
| 5878 | 5952 | if (!mgr->resource) { |
|
| 5879 | 5953 | pool_release(&workspace_manager_pool, mgr); |
|
| 5880 | 5954 | wl_client_post_no_memory(client); |
|
| 5881 | 5955 | return; |
|
| 5882 | 5956 | } |
|
| 5957 | + | wl_list_init(&mgr->groups); |
|
| 5883 | 5958 | wl_resource_set_implementation(mgr->resource, &ws_manager_impl, mgr, workspace_manager_destroy); |
|
| 5884 | 5959 | wl_list_insert(&ws_managers, &mgr->link); |
|
| 5960 | + | wl_list_for_each(m, &mons, link) workspace_group_create(mgr, m); |
|
| 5961 | + | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5962 | + | } |
|
| 5885 | 5963 | ||
| 5886 | - | mgr->group = wl_resource_create(client, &ext_workspace_group_handle_v1_interface, 1, 0); |
|
| 5964 | + | /* Add a group for a newly connected output to every workspace manager. */ |
|
| 5965 | + | void workspace_monitor_create(monitor_t *m) { |
|
| 5966 | + | workspace_manager_t *mgr; |
|
| 5887 | 5967 | ||
| 5888 | - | if (!mgr->group) { |
|
| 5889 | - | wl_client_post_no_memory(client); |
|
| 5890 | - | return; |
|
| 5968 | + | wl_list_for_each(mgr, &ws_managers, link) { |
|
| 5969 | + | workspace_group_create(mgr, m); |
|
| 5970 | + | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5891 | 5971 | } |
|
| 5892 | - | wl_resource_set_implementation(mgr->group, &ws_group_impl, mgr, workspace_group_destroy); |
|
| 5893 | - | ext_workspace_manager_v1_send_workspace_group(mgr->resource, mgr->group); |
|
| 5894 | - | ext_workspace_group_handle_v1_send_capabilities(mgr->group, 0); |
|
| 5895 | - | wl_list_for_each(m, &mons, link) { |
|
| 5896 | - | wl_resource_for_each(out, &m->wlr_output->resources) { |
|
| 5897 | - | if (wl_resource_get_client(out) == client) |
|
| 5898 | - | ext_workspace_group_handle_v1_send_output_enter(mgr->group, out); |
|
| 5972 | + | } |
|
| 5973 | + | ||
| 5974 | + | /* Remove a disconnected output's group from every workspace manager. */ |
|
| 5975 | + | void workspace_monitor_destroy(monitor_t *m) { |
|
| 5976 | + | workspace_manager_t *mgr; |
|
| 5977 | + | workspace_group_t *group, *group_tmp; |
|
| 5978 | + | workspace_handle_t *h, *h_tmp; |
|
| 5979 | + | struct wl_resource *out; |
|
| 5980 | + | ||
| 5981 | + | wl_list_for_each(mgr, &ws_managers, link) { |
|
| 5982 | + | if (mgr->pending_monitor == m) { |
|
| 5983 | + | mgr->pending = nullptr; |
|
| 5984 | + | mgr->pending_monitor = nullptr; |
|
| 5985 | + | } |
|
| 5986 | + | wl_list_for_each_safe(group, group_tmp, &mgr->groups, link) { |
|
| 5987 | + | if (group->mon != m) |
|
| 5988 | + | continue; |
|
| 5989 | + | wl_resource_for_each(out, &m->wlr_output->resources) { |
|
| 5990 | + | if (wl_resource_get_client(out) == wl_resource_get_client(mgr->resource)) |
|
| 5991 | + | ext_workspace_group_handle_v1_send_output_leave(group->resource, out); |
|
| 5992 | + | } |
|
| 5993 | + | wl_list_for_each_safe(h, h_tmp, &group->handles, group_link) { |
|
| 5994 | + | ext_workspace_group_handle_v1_send_workspace_leave(group->resource, h->resource); |
|
| 5995 | + | ext_workspace_handle_v1_send_removed(h->resource); |
|
| 5996 | + | wl_resource_set_user_data(h->resource, nullptr); |
|
| 5997 | + | wl_list_remove(&h->group_link); |
|
| 5998 | + | wl_list_remove(&h->link); |
|
| 5999 | + | pool_release(&workspace_handle_pool, h); |
|
| 6000 | + | } |
|
| 6001 | + | ext_workspace_group_handle_v1_send_removed(group->resource); |
|
| 6002 | + | wl_resource_set_user_data(group->resource, nullptr); |
|
| 6003 | + | wl_list_remove(&group->link); |
|
| 6004 | + | pool_release(&workspace_group_pool, group); |
|
| 6005 | + | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5899 | 6006 | } |
|
| 5900 | 6007 | } |
|
| 5901 | - | for (i = 0; i < WSCOUNT; i++) |
|
| 5902 | - | workspace_handle_create(mgr, &workspaces[i]); |
|
| 5903 | - | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5904 | 6008 | } |
|
| 5905 | 6009 | ||
| 5906 | - | /* Add a newly bound display to every published workspace group. */ |
|
| 6010 | + | /* Add a newly bound output resource to its published workspace group. */ |
|
| 5907 | 6011 | void workspace_output_bind(struct wl_listener *listener, void *data) { |
|
| 5908 | - | /* Tell workspace clients about the newly bound display. */ |
|
| 6012 | + | monitor_t *m = wl_container_of(listener, m, output_bind); |
|
| 5909 | 6013 | struct wlr_output_event_bind *event = data; |
|
| 5910 | 6014 | struct wl_client *client = wl_resource_get_client(event->resource); |
|
| 5911 | 6015 | workspace_manager_t *mgr; |
|
| 6016 | + | workspace_group_t *group; |
|
| 5912 | 6017 | ||
| 5913 | 6018 | wl_list_for_each(mgr, &ws_managers, link) { |
|
| 5914 | - | if (mgr->group && wl_resource_get_client(mgr->resource) == client) { |
|
| 5915 | - | ext_workspace_group_handle_v1_send_output_enter(mgr->group, event->resource); |
|
| 5916 | - | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 6019 | + | if (wl_resource_get_client(mgr->resource) != client) |
|
| 6020 | + | continue; |
|
| 6021 | + | wl_list_for_each(group, &mgr->groups, link) { |
|
| 6022 | + | if (group->mon == m) { |
|
| 6023 | + | ext_workspace_group_handle_v1_send_output_enter(group->resource, event->resource); |
|
| 6024 | + | ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 6025 | + | break; |
|
| 6026 | + | } |
|
| 5917 | 6027 | } |
|
| 5918 | 6028 | } |
|
| 5919 | 6029 | } |
|
| 5920 | 6030 | ||
| 5921 | - | /* Compute and publish a workspace's active, hidden, urgent, and focus state. */ |
|
| 5922 | - | uint32_t workspace_state(workspace_t *ws) { |
|
| 6031 | + | /* Compute a workspace's state relative to one output. */ |
|
| 6032 | + | uint32_t workspace_state(workspace_t *ws, monitor_t *m) { |
|
| 5923 | 6033 | _Static_assert( |
|
| 5924 | 6034 | (unsigned int)SWM_WORKSPACE_ACTIVE == (unsigned int)EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE, |
|
| 5925 | 6035 | "workspace active bit changed" |
|
| 5926 | 6036 | ); |
|
| 5927 | 6037 | _Static_assert( |
| 5941 | 6051 | if (c->ws != ws) |
|
| 5942 | 6052 | continue; |
|
| 5943 | 6053 | occupied = 1; |
|
| 5944 | 6054 | urgent |= c->is_urgent; |
|
| 5945 | 6055 | } |
|
| 5946 | - | /* ext-workspace-v1 has no occupied state. Mark empty, inactive |
|
| 5947 | - | * workspaces hidden so bars can omit them while retaining occupied and |
|
| 5948 | - | * currently selected workspaces. */ |
|
| 5949 | - | state = swm_workspace_state(ws->mon != nullptr, occupied, urgent); |
|
| 6056 | + | /* ext-workspace-v1 has no occupied state. Mark empty workspaces hidden |
|
| 6057 | + | * unless they are active on this output. */ |
|
| 6058 | + | state = swm_workspace_state(ws->mon == m, occupied, urgent); |
|
| 5950 | 6059 | return state; |
|
| 5951 | 6060 | } |
|
| 5952 | 6061 | ||
| 5953 | - | /* Publish current workspace state to every connected workspace client. */ |
|
| 6062 | + | /* Publish current per-output workspace state to every connected client. */ |
|
| 5954 | 6063 | void workspace_broadcast(void) { |
|
| 5955 | 6064 | workspace_manager_t *mgr; |
|
| 5956 | 6065 | workspace_handle_t *h; |
|
| 5957 | - | uint32_t state; |
|
| 5958 | 6066 | int i; |
|
| 5959 | 6067 | ||
| 5960 | 6068 | if (!wl_list_empty(&ws_managers)) { |
|
| 5961 | 6069 | for (i = 0; i < WSCOUNT; i++) { |
|
| 5962 | - | state = workspace_state(&workspaces[i]); |
|
| 5963 | - | wl_list_for_each(h, &workspaces[i].handles, link) |
|
| 5964 | - | ext_workspace_handle_v1_send_state(h->resource, state); |
|
| 6070 | + | wl_list_for_each(h, &workspaces[i].handles, link) { |
|
| 6071 | + | if (h->group) |
|
| 6072 | + | ext_workspace_handle_v1_send_state( |
|
| 6073 | + | h->resource, workspace_state(&workspaces[i], h->group->mon) |
|
| 6074 | + | ); |
|
| 6075 | + | } |
|
| 5965 | 6076 | } |
|
| 5966 | 6077 | wl_list_for_each(mgr, &ws_managers, link) ext_workspace_manager_v1_send_done(mgr->resource); |
|
| 5967 | 6078 | } |
|
| 5968 | 6079 | workspace_metadata_broadcast(); |
|
| 5969 | 6080 | } |
swmctl.1.adoc
+1 -0
| 57 | 57 | "return-type": "json", |
|
| 58 | 58 | "escape": false, |
|
| 59 | 59 | "hide-empty-text": true |
|
| 60 | 60 | } |
|
| 61 | 61 | ||
| 62 | + | ||
| 62 | 63 | The custom module follows the workspace on swm's selected output and uses its |
|
| 63 | 64 | color for the title text. |
|
| 64 | 65 | ||
| 65 | 66 | == EXAMPLES |
|
| 66 | 67 |
test/clients.py
+32 -0
| 607 | 607 | workspaces[target].activate() |
|
| 608 | 608 | manager.commit() |
|
| 609 | 609 | connection.roundtrips(2) |
|
| 610 | 610 | connection.close() |
|
| 611 | 611 | ||
| 612 | + | def workspace_groups(arguments: list[str]) -> None: |
|
| 613 | + | """Verify per-output workspace groups and active states.""" |
|
| 614 | + | ||
| 615 | + | if arguments: |
|
| 616 | + | raise Failure("workspace-groups takes no arguments") |
|
| 617 | + | connection = Connection(ExtWorkspaceManagerV1) |
|
| 618 | + | manager = connection.get(ExtWorkspaceManagerV1) |
|
| 619 | + | groups: list[object] = [] |
|
| 620 | + | workspaces: list[object] = [] |
|
| 621 | + | states: dict[object, int] = {} |
|
| 622 | + | ||
| 623 | + | def workspace_announced(proxy, handle): |
|
| 624 | + | workspaces.append(handle) |
|
| 625 | + | handle.dispatcher["name"] = lambda p, name: None |
|
| 626 | + | handle.dispatcher["state"] = lambda p, value: states.update({handle: value}) |
|
| 627 | + | handle.dispatcher["removed"] = lambda p: None |
|
| 628 | + | ||
| 629 | + | manager.dispatcher["workspace_group"] = lambda proxy, group: groups.append(group) |
|
| 630 | + | manager.dispatcher["workspace"] = workspace_announced |
|
| 631 | + | manager.dispatcher["done"] = lambda proxy: None |
|
| 632 | + | manager.dispatcher["finished"] = lambda proxy: None |
|
| 633 | + | connection.roundtrips(2) |
|
| 634 | + | if len(groups) != 2: |
|
| 635 | + | raise Failure(f"expected two output workspace groups, got {len(groups)}") |
|
| 636 | + | if len(workspaces) != 20: |
|
| 637 | + | raise Failure(f"expected 20 per-output workspace handles, got {len(workspaces)}") |
|
| 638 | + | active = sum(bool(states.get(handle, 0) & 1) for handle in workspaces) |
|
| 639 | + | if active != 2: |
|
| 640 | + | raise Failure(f"expected one active workspace per output, got {active}") |
|
| 641 | + | connection.close() |
|
| 642 | + | ||
| 612 | 643 | ||
| 613 | 644 | def output_power(arguments: list[str]) -> None: |
|
| 614 | 645 | """Turn one output off and back on through output-power-v1.""" |
|
| 615 | 646 | ||
| 616 | 647 | connection = Connection(WlOutput, ZwlrOutputPowerManagerV1) |
| 1111 | 1142 | "transient": transient, |
|
| 1112 | 1143 | "keyboard": keyboard, |
|
| 1113 | 1144 | "pointer": pointer, |
|
| 1114 | 1145 | "foreign": foreign, |
|
| 1115 | 1146 | "workspace": workspace, |
|
| 1147 | + | "workspace-groups": workspace_groups, |
|
| 1116 | 1148 | "output-power": output_power, |
|
| 1117 | 1149 | "output-management": output_management, |
|
| 1118 | 1150 | "layer": layer, |
|
| 1119 | 1151 | "session-lock": session_lock, |
|
| 1120 | 1152 | "text-input": text_input, |
test/run.py
+2 -0
| 710 | 710 | """Exercise workspace movement and output removal.""" |
|
| 711 | 711 | ||
| 712 | 712 | client = start_client("multi", "ff778899") |
|
| 713 | 713 | ||
| 714 | 714 | def multioutput() -> None: |
|
| 715 | + | protocol("workspace-groups") |
|
| 715 | 716 | virtual_keyboard(105, 1) |
|
| 716 | 717 | wait_title("") |
|
| 717 | 718 | virtual_keyboard(106, 1) |
|
| 718 | 719 | wait_title("multi") |
|
| 719 | 720 | virtual_keyboard(105, 4) |
| 723 | 724 | protocol("output-management", "configure-second") |
|
| 724 | 725 | protocol("output-management", "disable-second") |
|
| 725 | 726 | wait_title("multi") |
|
| 726 | 727 | protocol("output-management", "enable-second") |
|
| 727 | 728 | wait_title("multi") |
|
| 729 | + | protocol("workspace-groups") |
|
| 728 | 730 | virtual_keyboard(106, 4) |
|
| 729 | 731 | wait_title("") |
|
| 730 | 732 | virtual_keyboard(106, 1) |
|
| 731 | 733 | wait_title("multi") |
|
| 732 | 734 |
test/unit.c
+9 -6
| 85 | 85 | test_pool(&session_lock_pool); |
|
| 86 | 86 | test_pool(&pending_spawn_pool); |
|
| 87 | 87 | test_pool(&window_state_pool); |
|
| 88 | 88 | test_pool(&static_listener_pool); |
|
| 89 | 89 | test_pool(&workspace_manager_pool); |
|
| 90 | + | test_pool(&workspace_group_pool); |
|
| 90 | 91 | test_pool(&workspace_handle_pool); |
|
| 91 | 92 | test_pool(&metadata_manager_pool); |
|
| 92 | 93 | } |
|
| 93 | 94 | ||
| 94 | 95 | /* Require a geometry helper to return the expected rectangle. */ |
| 217 | 218 | ||
| 218 | 219 | /* Verify free-workspace selection and published workspace states. */ |
|
| 219 | 220 | static void test_workspace_queries(void) { |
|
| 220 | 221 | client_t *client; |
|
| 221 | 222 | workspace_t *workspace; |
|
| 223 | + | monitor_t first = {}, second = {}; |
|
| 222 | 224 | int i; |
|
| 223 | 225 | ||
| 224 | 226 | for (i = 0; i < WSCOUNT; i++) { |
|
| 225 | - | workspaces[i].mon = (monitor_t *)1; |
|
| 227 | + | workspaces[i].mon = &first; |
|
| 226 | 228 | wl_list_init(&workspaces[i].handles); |
|
| 227 | 229 | } |
|
| 228 | 230 | workspaces[4].mon = nullptr; |
|
| 229 | 231 | assert(free_workspace() == &workspaces[4]); |
|
| 230 | - | workspaces[4].mon = (monitor_t *)1; |
|
| 232 | + | workspaces[4].mon = &first; |
|
| 231 | 233 | assert(free_workspace() == nullptr); |
|
| 232 | 234 | ||
| 233 | 235 | wl_list_init(&clients); |
|
| 234 | 236 | workspace = &workspaces[2]; |
|
| 235 | 237 | workspace->mon = nullptr; |
|
| 236 | - | assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN); |
|
| 238 | + | assert(workspace_state(workspace, &first) == EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN); |
|
| 237 | 239 | client = pool_take(&client_pool); |
|
| 238 | 240 | assert(client); |
|
| 239 | 241 | client->ws = workspace; |
|
| 240 | 242 | client->is_urgent = 1; |
|
| 241 | 243 | wl_list_insert(&clients, &client->link); |
|
| 242 | - | assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_URGENT); |
|
| 243 | - | workspace->mon = (monitor_t *)1; |
|
| 244 | + | assert(workspace_state(workspace, &first) == EXT_WORKSPACE_HANDLE_V1_STATE_URGENT); |
|
| 245 | + | workspace->mon = &first; |
|
| 244 | 246 | assert( |
|
| 245 | - | workspace_state(workspace) == |
|
| 247 | + | workspace_state(workspace, &first) == |
|
| 246 | 248 | (EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE | EXT_WORKSPACE_HANDLE_V1_STATE_URGENT) |
|
| 247 | 249 | ); |
|
| 250 | + | assert(workspace_state(workspace, &second) == EXT_WORKSPACE_HANDLE_V1_STATE_URGENT); |
|
| 248 | 251 | wl_list_remove(&client->link); |
|
| 249 | 252 | pool_release(&client_pool, client); |
|
| 250 | 253 | } |
|
| 251 | 254 | ||
| 252 | 255 | /* Verify state-file paths and client border-width policy. */ |