1
0
Fork 0
mirror of https://github.com/yshui/picom.git synced 2025-04-14 17:53:25 -04:00

wm: add initial data structure for managing the window tree

Signed-off-by: Yuxuan Shui <yshuiv7@gmail.com>
This commit is contained in:
Yuxuan Shui 2024-06-04 00:50:45 +01:00
parent 88c8ba8c5e
commit 5ecac66185
No known key found for this signature in database
GPG key ID: D3A4405BE6CC17F4
5 changed files with 650 additions and 2 deletions

View file

@ -64,6 +64,8 @@ safe_isinf(double a) {
abort(); \
} \
} while (0)
/// Abort the program if `expr` is NULL. This is NOT disabled in release builds.
#define BUG_ON_NULL(expr) BUG_ON((expr) == NULL);
#define CHECK_EXPR(...) ((void)0)
/// Same as assert, but evaluates the expression even in release builds
#define CHECK(expr) \

View file

@ -1 +1 @@
srcs += [ files('win.c', 'wm.c') ]
srcs += [ files('win.c', 'wm.c', 'tree.c') ]

519
src/wm/tree.c Normal file
View file

@ -0,0 +1,519 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) Yuxuan Shui <yshuiv7@gmail.com>
/// In my ideal world, the compositor shouldn't be concerned with the X window tree. It
/// should only need to care about the toplevel windows. However, because we support
/// window rules based on window properties, which can be set on any descendant of a
/// toplevel, we need to keep track of the entire window tree.
///
/// For descendants of a toplevel window, what we actually care about is what's called a
/// "client" window. A client window is a window with the `WM_STATE` property set, in
/// theory and descendants of a toplevel can gain/lose this property at any time. So we
/// setup a minimal structure for every single window to keep track of this. And once
/// a window becomes a client window, it will have our full attention and have all of its
/// information stored in the toplevel `struct managed_win`.
#include <uthash.h>
#include <xcb/xproto.h>
#include "log.h"
#include "utils/list.h"
#include "utils/misc.h"
#include "wm.h"
#include "wm_internal.h"
struct wm_tree_change_list {
struct wm_tree_change item;
struct list_node siblings;
};
void wm_tree_reap_zombie(struct wm_tree_node *zombie) {
BUG_ON(!zombie->is_zombie);
list_remove(&zombie->siblings);
free(zombie);
}
static void wm_tree_enqueue_change(struct wm_tree *tree, struct wm_tree_change change) {
if (change.type == WM_TREE_CHANGE_TOPLEVEL_KILLED) {
// A gone toplevel will cancel out a previous
// `WM_TREE_CHANGE_TOPLEVEL_NEW` change in the queue.
bool found = false;
list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) {
if (i->item.type == WM_TREE_CHANGE_TOPLEVEL_NEW &&
wm_treeid_eq(i->item.toplevel, change.toplevel)) {
list_remove(&i->siblings);
list_insert_after(&tree->free_changes, &i->siblings);
found = true;
} else if (wm_treeid_eq(i->item.toplevel, change.toplevel) && found) {
// We also need to delete all other changes related to
// this toplevel in between the new and gone changes.
list_remove(&i->siblings);
list_insert_after(&tree->free_changes, &i->siblings);
}
}
if (found) {
wm_tree_reap_zombie(change.killed);
return;
}
} else if (change.type == WM_TREE_CHANGE_CLIENT) {
// A client change can coalesce with a previous client change.
list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) {
if (!wm_treeid_eq(i->item.toplevel, change.toplevel) ||
i->item.type != WM_TREE_CHANGE_CLIENT) {
continue;
}
if (!wm_treeid_eq(i->item.client.new_, change.client.old)) {
log_warn("Inconsistent client change for toplevel "
"%#010x. Missing changes from %#010x to %#010x. "
"Possible bug.",
change.toplevel.x, i->item.client.new_.x,
change.client.old.x);
}
i->item.client.new_ = change.client.new_;
if (wm_treeid_eq(i->item.client.old, change.client.new_)) {
list_remove(&i->siblings);
list_insert_after(&tree->free_changes, &i->siblings);
}
return;
}
} else if (change.type == WM_TREE_CHANGE_TOPLEVEL_RESTACKED) {
list_foreach(struct wm_tree_change_list, i, &tree->changes, siblings) {
if (i->item.type == WM_TREE_CHANGE_TOPLEVEL_RESTACKED ||
i->item.type == WM_TREE_CHANGE_TOPLEVEL_NEW ||
i->item.type == WM_TREE_CHANGE_TOPLEVEL_KILLED) {
// Only need to keep one
// `WM_TREE_CHANGE_TOPLEVEL_RESTACKED` change, and order
// doesn't matter.
return;
}
}
}
// We don't let a `WM_TREE_CHANGE_TOPLEVEL_NEW` cancel out a previous
// `WM_TREE_CHANGE_TOPLEVEL_GONE`, because the new toplevel would be a different
// window reusing the same ID. So we need to go through the proper destruction
// process for the previous toplevel. Changes are not commutative (naturally).
struct wm_tree_change_list *change_list;
if (!list_is_empty(&tree->free_changes)) {
change_list = list_entry(tree->free_changes.next,
struct wm_tree_change_list, siblings);
list_remove(&change_list->siblings);
} else {
change_list = cmalloc(struct wm_tree_change_list);
}
change_list->item = change;
list_insert_before(&tree->changes, &change_list->siblings);
}
/// Dequeue the oldest change from the change queue. If the queue is empty, a change with
/// `toplevel` set to `XCB_NONE` will be returned.
struct wm_tree_change wm_tree_dequeue_change(struct wm_tree *tree) {
if (list_is_empty(&tree->changes)) {
return (struct wm_tree_change){.type = WM_TREE_CHANGE_NONE};
}
auto change = list_entry(tree->changes.next, struct wm_tree_change_list, siblings);
list_remove(&change->siblings);
list_insert_after(&tree->free_changes, &change->siblings);
return change->item;
}
/// Return the next node in the subtree after `node` in a pre-order traversal. Returns
/// NULL if `node` is the last node in the traversal.
static struct wm_tree_node *
wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot) {
if (!list_is_empty(&node->children)) {
// Descend if there are children
return list_entry(node->children.next, struct wm_tree_node, siblings);
}
while (node != subroot && node->siblings.next == &node->parent->children) {
// If the current node has no more children, go back to the
// parent.
node = node->parent;
}
if (node == subroot) {
// We've gone past the topmost node for our search, stop.
return NULL;
}
return list_entry(node->siblings.next, struct wm_tree_node, siblings);
}
/// Find a client window under a toplevel window. If there are multiple windows with
/// `WM_STATE` set under the toplevel window, we will return an arbitrary one.
static struct wm_tree_node *attr_pure wm_tree_find_client(struct wm_tree_node *subroot) {
if (subroot->has_wm_state) {
log_debug("Toplevel %#010x has WM_STATE set, weird. Using itself as its "
"client window.",
subroot->id.x);
return subroot;
}
BUG_ON(subroot->parent == NULL); // Trying to find client window on the
// root window
for (auto curr = subroot; curr != NULL; curr = wm_tree_next(curr, subroot)) {
if (curr->has_wm_state) {
return curr;
}
}
return NULL;
}
struct wm_tree_node *wm_tree_find(const struct wm_tree *tree, xcb_window_t id) {
struct wm_tree_node *node = NULL;
HASH_FIND_INT(tree->nodes, &id, node);
return node;
}
struct wm_tree_node *wm_tree_find_toplevel_for(struct wm_tree_node *node) {
BUG_ON_NULL(node);
BUG_ON_NULL(node->parent); // Trying to find toplevel for the root
// window
struct wm_tree_node *toplevel;
for (auto curr = node; curr->parent != NULL; curr = curr->parent) {
toplevel = curr;
}
return toplevel;
}
/// Change whether a tree node has the `WM_STATE` property set.
/// `destroyed` indicate whether `node` is about to be destroyed, in which case, the `old`
/// field of the change event will be set to NULL.
void wm_tree_set_wm_state(struct wm_tree *tree, struct wm_tree_node *node, bool has_wm_state) {
BUG_ON(node == NULL);
if (node->has_wm_state == has_wm_state) {
log_debug("WM_STATE unchanged call (window %#010x, WM_STATE %d).",
node->id.x, has_wm_state);
return;
}
node->has_wm_state = has_wm_state;
BUG_ON(node->parent == NULL); // Trying to set WM_STATE on the root window
struct wm_tree_node *toplevel;
for (auto cur = node; cur->parent != NULL; cur = cur->parent) {
toplevel = cur;
}
if (toplevel == node) {
log_debug("Setting WM_STATE on a toplevel window %#010x, weird.", node->id.x);
return;
}
struct wm_tree_change change = {
.toplevel = toplevel->id,
.type = WM_TREE_CHANGE_CLIENT,
.client = {.toplevel = toplevel},
};
if (!has_wm_state && toplevel->client_window == node) {
auto new_client = wm_tree_find_client(toplevel);
toplevel->client_window = new_client;
change.client.old = node->id;
change.client.new_ = new_client != NULL ? new_client->id : WM_TREEID_NONE;
wm_tree_enqueue_change(tree, change);
} else if (has_wm_state && toplevel->client_window == NULL) {
toplevel->client_window = node;
change.client.old = WM_TREEID_NONE;
change.client.new_ = node->id;
wm_tree_enqueue_change(tree, change);
} else if (has_wm_state) {
// If the toplevel window already has a client window, we won't
// try to usurp it.
log_debug("Toplevel window %#010x already has a client window "
"%#010x, ignoring new client window %#010x. I don't "
"like your window manager.",
toplevel->id.x, toplevel->client_window->id.x, node->id.x);
}
}
struct wm_tree_node *
wm_tree_new_window(struct wm_tree *tree, xcb_window_t id, struct wm_tree_node *parent) {
auto node = ccalloc(1, struct wm_tree_node);
node->id.x = id;
node->id.gen = tree->gen++;
node->has_wm_state = false;
list_init_head(&node->children);
BUG_ON(parent == NULL && tree->nodes != NULL); // Trying to create a second
// root window
HASH_ADD_INT(tree->nodes, id.x, node);
node->parent = parent;
if (parent != NULL) {
list_insert_after(&parent->children, &node->siblings);
if (parent->parent == NULL) {
// Parent is root, this is a new toplevel window
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.toplevel = node->id,
.type = WM_TREE_CHANGE_TOPLEVEL_NEW,
.new_ = node,
});
}
}
return node;
}
static void
wm_tree_refresh_client_and_queue_change(struct wm_tree *tree, struct wm_tree_node *toplevel) {
BUG_ON_NULL(toplevel);
BUG_ON_NULL(toplevel->parent);
BUG_ON(toplevel->parent->parent != NULL);
auto new_client = wm_tree_find_client(toplevel);
if (new_client != toplevel->client_window) {
struct wm_tree_change change = {.toplevel = toplevel->id,
.type = WM_TREE_CHANGE_CLIENT,
.client = {.toplevel = toplevel,
.old = WM_TREEID_NONE,
.new_ = WM_TREEID_NONE}};
if (toplevel->client_window != NULL) {
change.client.old = toplevel->client_window->id;
}
if (new_client != NULL) {
change.client.new_ = new_client->id;
}
toplevel->client_window = new_client;
wm_tree_enqueue_change(tree, change);
}
}
void wm_tree_detach(struct wm_tree *tree, struct wm_tree_node *subroot) {
BUG_ON(subroot == NULL);
BUG_ON(subroot->parent == NULL); // Trying to detach the root window?!
auto toplevel = wm_tree_find_toplevel_for(subroot);
if (toplevel != subroot) {
list_remove(&subroot->siblings);
wm_tree_refresh_client_and_queue_change(tree, toplevel);
} else {
// Detached a toplevel, create a zombie for it
auto zombie = ccalloc(1, struct wm_tree_node);
zombie->parent = subroot->parent;
zombie->id = subroot->id;
zombie->is_zombie = true;
zombie->win = subroot->win;
list_init_head(&zombie->children);
list_replace(&subroot->siblings, &zombie->siblings);
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.toplevel = subroot->id,
.type = WM_TREE_CHANGE_TOPLEVEL_KILLED,
.killed = zombie,
});
}
}
void wm_tree_destroy_window(struct wm_tree *tree, struct wm_tree_node *node) {
BUG_ON(node == NULL);
BUG_ON(node->parent == NULL); // Trying to destroy the root window?!
if (node->has_wm_state) {
wm_tree_set_wm_state(tree, node, false);
}
HASH_DEL(tree->nodes, node);
if (!list_is_empty(&node->children)) {
log_error("Window %#010x is destroyed, but it still has children. Expect "
"malfunction.",
node->id.x);
list_foreach_safe(struct wm_tree_node, i, &node->children, siblings) {
wm_tree_destroy_window(tree, i);
}
}
if (node->parent->parent == NULL) {
node->is_zombie = true;
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.toplevel = node->id,
.type = WM_TREE_CHANGE_TOPLEVEL_KILLED,
.killed = node,
});
} else {
list_remove(&node->siblings);
free(node);
}
}
/// Move `node` to the top or the bottom of its parent's child window stack.
void wm_tree_move_to_end(struct wm_tree *tree, struct wm_tree_node *node, bool to_bottom) {
BUG_ON(node == NULL);
BUG_ON(node->parent == NULL); // Trying to move the root window
if ((node->parent->children.next == &node->siblings && !to_bottom) ||
(node->parent->children.prev == &node->siblings && to_bottom)) {
// Already at the target position
return;
}
list_remove(&node->siblings);
if (to_bottom) {
list_insert_before(&node->parent->children, &node->siblings);
} else {
list_insert_after(&node->parent->children, &node->siblings);
}
if (node->parent->parent == NULL) {
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.type = WM_TREE_CHANGE_TOPLEVEL_RESTACKED,
});
}
}
/// Move `node` to above `other` in their parent's child window stack.
void wm_tree_move_to_above(struct wm_tree *tree, struct wm_tree_node *node,
struct wm_tree_node *other) {
BUG_ON(node == NULL);
BUG_ON(node->parent == NULL); // Trying to move the root window
BUG_ON(other == NULL);
BUG_ON(node->parent != other->parent);
if (node->siblings.next == &other->siblings) {
// Already above `other`
return;
}
list_remove(&node->siblings);
list_insert_before(&other->siblings, &node->siblings);
if (node->parent->parent == NULL) {
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.type = WM_TREE_CHANGE_TOPLEVEL_RESTACKED,
});
}
}
void wm_tree_reparent(struct wm_tree *tree, struct wm_tree_node *node,
struct wm_tree_node *new_parent) {
BUG_ON(node == NULL);
BUG_ON(new_parent == NULL); // Trying make `node` a root window
if (node->parent == new_parent) {
// Reparent to the same parent moves the window to the top of the stack
wm_tree_move_to_end(tree, node, false);
return;
}
wm_tree_detach(tree, node);
// Reparented window always becomes the topmost child of the new parent
list_insert_after(&new_parent->children, &node->siblings);
node->parent = new_parent;
auto toplevel = wm_tree_find_toplevel_for(node);
if (node == toplevel) {
// This node could have a stale `->win` if it was a toplevel at
// some point in the past.
node->win = NULL;
wm_tree_enqueue_change(tree, (struct wm_tree_change){
.toplevel = node->id,
.type = WM_TREE_CHANGE_TOPLEVEL_NEW,
.new_ = node,
});
} else {
wm_tree_refresh_client_and_queue_change(tree, toplevel);
}
}
void wm_tree_clear(struct wm_tree *tree) {
struct wm_tree_node *cur, *tmp;
HASH_ITER(hh, tree->nodes, cur, tmp) {
HASH_DEL(tree->nodes, cur);
free(cur);
}
list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) {
list_remove(&i->siblings);
free(i);
}
list_foreach_safe(struct wm_tree_change_list, i, &tree->free_changes, siblings) {
list_remove(&i->siblings);
free(i);
}
}
TEST_CASE(tree_manipulation) {
struct wm_tree tree;
wm_tree_init(&tree);
wm_tree_new_window(&tree, 1, NULL);
auto root = wm_tree_find(&tree, 1);
assert(root != NULL);
assert(root->parent == NULL);
auto change = wm_tree_dequeue_change(&tree);
assert(change.type == WM_TREE_CHANGE_NONE);
wm_tree_new_window(&tree, 2, root);
auto node2 = wm_tree_find(&tree, 2);
assert(node2 != NULL);
assert(node2->parent == root);
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 2);
assert(change.type == WM_TREE_CHANGE_TOPLEVEL_NEW);
assert(wm_treeid_eq(node2->id, change.toplevel));
wm_tree_new_window(&tree, 3, root);
auto node3 = wm_tree_find(&tree, 3);
assert(node3 != NULL);
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 3);
assert(change.type == WM_TREE_CHANGE_TOPLEVEL_NEW);
wm_tree_reparent(&tree, node2, node3);
assert(node2->parent == node3);
assert(node3->children.next == &node2->siblings);
// node2 is now a child of node3, so it's no longer a toplevel
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 2);
assert(change.type == WM_TREE_CHANGE_TOPLEVEL_KILLED);
wm_tree_reap_zombie(change.killed);
wm_tree_set_wm_state(&tree, node2, true);
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 3);
assert(change.type == WM_TREE_CHANGE_CLIENT);
assert(wm_treeid_eq(change.client.old, WM_TREEID_NONE));
assert(change.client.new_.x == 2);
wm_tree_new_window(&tree, 4, node3);
auto node4 = wm_tree_find(&tree, 4);
change = wm_tree_dequeue_change(&tree);
assert(change.type == WM_TREE_CHANGE_NONE);
wm_tree_set_wm_state(&tree, node4, true);
change = wm_tree_dequeue_change(&tree);
// node3 already has node2 as its client window, so the new one should be ignored.
assert(change.type == WM_TREE_CHANGE_NONE);
wm_tree_destroy_window(&tree, node2);
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 3);
assert(change.type == WM_TREE_CHANGE_CLIENT);
assert(change.client.old.x == 2);
assert(change.client.new_.x == 4);
// Test window ID reuse
wm_tree_destroy_window(&tree, node4);
node4 = wm_tree_new_window(&tree, 4, node3);
wm_tree_set_wm_state(&tree, node4, true);
change = wm_tree_dequeue_change(&tree);
assert(change.toplevel.x == 3);
assert(change.type == WM_TREE_CHANGE_CLIENT);
assert(change.client.old.x == 4);
assert(change.client.new_.x == 4);
auto node5 = wm_tree_new_window(&tree, 5, root);
wm_tree_destroy_window(&tree, node5);
change = wm_tree_dequeue_change(&tree);
assert(change.type == WM_TREE_CHANGE_NONE); // Changes cancelled out
wm_tree_clear(&tree);
}

View file

@ -32,6 +32,31 @@ struct subwin {
UT_hash_handle hh;
};
enum wm_tree_change_type {
/// The client window of a toplevel changed
WM_TREE_CHANGE_CLIENT,
/// A toplevel window is killed on the X server side
/// A zombie will be left in its place.
WM_TREE_CHANGE_TOPLEVEL_KILLED,
/// A new toplevel window appeared
WM_TREE_CHANGE_TOPLEVEL_NEW,
// TODO(yshui): This is a stop-gap measure to make sure we invalidate `reg_ignore`
// of windows. Once we get rid of `reg_ignore`, which is only used by the legacy
// backends, this event should be removed.
//
// (`reg_ignore` is the cached cumulative opaque region of all windows above a
// window in the stacking order. If it actually is performance critical, we
// can probably cache it more cleanly in renderer layout.)
/// The stacking order of toplevel windows changed. Note, toplevel gone/new
/// changes also imply a restack.
WM_TREE_CHANGE_TOPLEVEL_RESTACKED,
/// Nothing changed
WM_TREE_CHANGE_NONE,
};
struct wm *wm_new(void);
void wm_free(struct wm *wm, struct x_connection *c);
@ -42,7 +67,7 @@ void wm_set_active_leader(struct wm *wm, xcb_window_t leader);
// Note: `wm` keeps track of 2 lists of windows. One is the window stack, which includes
// all windows that might need to be rendered, which means it would include destroyed
// windows in case they need to be faded out. This list is accessed by `wm_stack_*` series
// windows in case they have close animation. This list is accessed by `wm_stack_*` series
// of functions. The other is a hash table of windows, which does not include destroyed
// windows. This list is accessed by `wm_find_*`, `wm_foreach`, and `wm_num_windows`.
// Adding a window to the window stack also automatically adds it to the hash table.

102
src/wm/wm_internal.h Normal file
View file

@ -0,0 +1,102 @@
#pragma once
#include <assert.h>
#include <stdalign.h>
#include <uthash.h>
#include <xcb/xproto.h>
#include "utils/list.h"
#include "wm.h"
struct wm_tree {
/// The generation of the wm tree. This number is incremented every time a new
/// window is created.
uint64_t gen;
/// wm tree nodes indexed by their X window ID.
struct wm_tree_node *nodes;
struct list_node changes;
struct list_node free_changes;
};
typedef struct wm_treeid {
/// The generation of the window ID. This is used to detect if the window ID is
/// reused. Inherited from the wm_tree at cr
uint64_t gen;
/// The X window ID.
xcb_window_t x;
/// Explicit padding
char padding[4];
} wm_treeid;
static const wm_treeid WM_TREEID_NONE = {.gen = 0, .x = XCB_NONE};
static_assert(sizeof(wm_treeid) == 16, "wm_treeid size is not 16 bytes");
static_assert(alignof(wm_treeid) == 8, "wm_treeid alignment is not 8 bytes");
struct wm_tree_node {
UT_hash_handle hh;
struct wm_tree_node *parent;
struct win *win;
struct list_node siblings;
struct list_node children;
wm_treeid id;
/// The client window. Only a toplevel can have a client window.
struct wm_tree_node *client_window;
bool has_wm_state : 1;
/// Whether this window exists only on our side. A zombie window is a toplevel
/// that has been destroyed or reparented (i.e. no long a toplevel) on the X
/// server side, but is kept on our side for things like animations. A zombie
/// window cannot be found in the wm_tree hash table.
bool is_zombie : 1;
};
/// Describe a change of a toplevel's client window.
/// A `XCB_NONE` in either `old_client` or `new_client` means a missing client window.
/// i.e. if `old_client` is `XCB_NONE`, it means the toplevel window did not have a client
/// window before the change, and if `new_client` is `XCB_NONE`, it means the toplevel
/// window lost its client window after the change.
struct wm_tree_change {
wm_treeid toplevel;
union {
/// Information for `WM_TREE_CHANGE_CLIENT`.
struct {
struct wm_tree_node *toplevel;
/// The old and new client windows.
wm_treeid old, new_;
} client;
/// Information for `WM_TREE_CHANGE_TOPLEVEL_KILLED`.
/// The zombie window left in place of the killed toplevel.
struct wm_tree_node *killed;
struct wm_tree_node *new_;
};
enum wm_tree_change_type type;
};
/// Free all tree nodes and changes, without generating any change events. Used when
/// shutting down.
void wm_tree_clear(struct wm_tree *tree);
struct wm_tree_node *wm_tree_find(struct wm_tree *tree, xcb_window_t id);
struct wm_tree_node *wm_tree_find_toplevel_for(struct wm_tree_node *node);
/// Detach the subtree rooted at `subroot` from `tree`. The subtree root is removed from
/// its parent, and the disconnected tree nodes won't be able to be found via
/// `wm_tree_find`. Relevant events will be generated.
void wm_tree_detach(struct wm_tree *tree, struct wm_tree_node *subroot);
static inline void wm_tree_init(struct wm_tree *tree) {
tree->nodes = NULL;
list_init_head(&tree->changes);
list_init_head(&tree->free_changes);
}
static inline bool wm_treeid_eq(wm_treeid a, wm_treeid b) {
return a.gen == b.gen && a.x == b.x;
}