Improve multi-output support

059c81bfb6872ca99fd021dcfe50c396debe5ebd
Alexis Sellier committed ago 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. */