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:
parent
88c8ba8c5e
commit
5ecac66185
5 changed files with 650 additions and 2 deletions
|
@ -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) \
|
||||
|
|
|
@ -1 +1 @@
|
|||
srcs += [ files('win.c', 'wm.c') ]
|
||||
srcs += [ files('win.c', 'wm.c', 'tree.c') ]
|
||||
|
|
519
src/wm/tree.c
Normal file
519
src/wm/tree.c
Normal 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);
|
||||
}
|
27
src/wm/wm.h
27
src/wm/wm.h
|
@ -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
102
src/wm/wm_internal.h
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue