summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/toplevel.cpp
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 19:24:22 +0900
committerAdam Malczewski <[email protected]>2026-06-12 19:24:22 +0900
commit8d7749516d70b8a27df4441c2b3e717de1a7a724 (patch)
tree9b0cb1257cd6d7859972b990710ebf69ac293db4 /packages/kernel/src/toplevel.cpp
parenta21f705692595ea711a736e2ae9c256c1dde7b1e (diff)
downloadunbox-8d7749516d70b8a27df4441c2b3e717de1a7a724.tar.gz
unbox-8d7749516d70b8a27df4441c2b3e717de1a7a724.zip
Slice 2: tinywl port — kernel compositor runs nested, manages toplevels, touch added
Server contract (pimpl, create/run/dispatch/terminate) over a faithful tinywl 0.20.1 port: outputs via wlr_scene, xdg-shell toplevels+popups, focus, interactive move/resize, keyboard/pointer through wlr_cursor — plus touch (down/up/motion/cancel/frame via seat notifies with per-point origin tracking), which tinywl lacks. RAII Listener replaces manual wl_list_remove bookkeeping; shutdown ordering documented in kernel.md. xkbcommon added as a system dep. Verified: nested under labwc (output WL-1, foot mapped and focused on GLES2) and a headless+pixman boot test in the kernel suite.
Diffstat (limited to 'packages/kernel/src/toplevel.cpp')
-rw-r--r--packages/kernel/src/toplevel.cpp190
1 files changed, 190 insertions, 0 deletions
diff --git a/packages/kernel/src/toplevel.cpp b/packages/kernel/src/toplevel.cpp
new file mode 100644
index 0000000..150870d
--- /dev/null
+++ b/packages/kernel/src/toplevel.cpp
@@ -0,0 +1,190 @@
+#include "server_impl.hpp"
+
+#include <algorithm>
+
+namespace unbox::kernel {
+
+// ---- Focus ------------------------------------------------------------------
+
+void Server::Impl::focus_toplevel(Toplevel* toplevel) {
+ // Keyboard focus only (pointer focus follows the cursor).
+ if (toplevel == nullptr) {
+ return;
+ }
+ wlr_surface* surface = toplevel->xdg_toplevel->base->surface;
+ wlr_surface* prev_surface = seat->keyboard_state.focused_surface;
+ if (prev_surface == surface) {
+ return;
+ }
+ if (prev_surface != nullptr) {
+ // Deactivate the previously focused toplevel (client stops drawing
+ // its focused decoration state, e.g. hides the caret).
+ wlr_xdg_toplevel* prev = wlr_xdg_toplevel_try_from_wlr_surface(prev_surface);
+ if (prev != nullptr) {
+ wlr_xdg_toplevel_set_activated(prev, false);
+ }
+ }
+
+ wlr_scene_node_raise_to_top(&toplevel->scene_tree->node);
+ auto it = std::find(mapped_toplevels.begin(), mapped_toplevels.end(), toplevel);
+ if (it != mapped_toplevels.end()) {
+ mapped_toplevels.splice(mapped_toplevels.begin(), mapped_toplevels, it);
+ }
+ wlr_xdg_toplevel_set_activated(toplevel->xdg_toplevel, true);
+
+ // The seat tracks the focused surface and routes key events to it.
+ if (wlr_keyboard* keyboard = wlr_seat_get_keyboard(seat)) {
+ wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes,
+ keyboard->num_keycodes, &keyboard->modifiers);
+ }
+}
+
+auto Server::Impl::toplevel_at(double lx, double ly, wlr_surface** surface, double* sx, double* sy)
+ -> Toplevel* {
+ // Topmost scene node at the given layout coords; we only care about
+ // buffer nodes belonging to a surface tree rooted at a Toplevel.
+ wlr_scene_node* node = wlr_scene_node_at(&scene->tree.node, lx, ly, sx, sy);
+ if (node == nullptr || node->type != WLR_SCENE_NODE_BUFFER) {
+ return nullptr;
+ }
+ wlr_scene_buffer* scene_buffer = wlr_scene_buffer_from_node(node);
+ wlr_scene_surface* scene_surface = wlr_scene_surface_try_from_buffer(scene_buffer);
+ if (scene_surface == nullptr) {
+ return nullptr;
+ }
+ *surface = scene_surface->surface;
+
+ // Walk up to the tree whose data field we set: the Toplevel root.
+ wlr_scene_tree* tree = node->parent;
+ while (tree != nullptr && tree->node.data == nullptr) {
+ tree = tree->node.parent;
+ }
+ if (tree == nullptr) {
+ return nullptr;
+ }
+ return static_cast<Toplevel*>(tree->node.data);
+}
+
+// ---- Interactive move/resize grabs -------------------------------------------
+
+void Server::Impl::reset_cursor_mode() {
+ cursor_mode = CursorMode::Passthrough;
+ grabbed_toplevel = nullptr;
+}
+
+void Server::Impl::begin_interactive(Toplevel* toplevel, CursorMode mode, std::uint32_t edges) {
+ // The compositor consumes pointer events itself during a grab instead
+ // of forwarding them. (tinywl note kept: a fuller compositor would
+ // verify this against a recent button-press serial.)
+ grabbed_toplevel = toplevel;
+ cursor_mode = mode;
+
+ if (mode == CursorMode::Move) {
+ grab_x = cursor->x - toplevel->scene_tree->node.x;
+ grab_y = cursor->y - toplevel->scene_tree->node.y;
+ } else {
+ wlr_box* geo_box = &toplevel->xdg_toplevel->base->geometry;
+ const double border_x = (toplevel->scene_tree->node.x + geo_box->x) +
+ ((edges & WLR_EDGE_RIGHT) != 0 ? geo_box->width : 0);
+ const double border_y = (toplevel->scene_tree->node.y + geo_box->y) +
+ ((edges & WLR_EDGE_BOTTOM) != 0 ? geo_box->height : 0);
+ grab_x = cursor->x - border_x;
+ grab_y = cursor->y - border_y;
+
+ grab_geobox = *geo_box;
+ grab_geobox.x += toplevel->scene_tree->node.x;
+ grab_geobox.y += toplevel->scene_tree->node.y;
+ resize_edges = edges;
+ }
+}
+
+// ---- xdg-shell toplevels ------------------------------------------------------
+
+void Server::Impl::handle_new_toplevel(wlr_xdg_toplevel* xdg_toplevel) {
+ auto owned = std::make_unique<Toplevel>();
+ Toplevel* toplevel = owned.get();
+ toplevel->server = this;
+ toplevel->xdg_toplevel = xdg_toplevel;
+ toplevel->scene_tree = wlr_scene_xdg_surface_create(&scene->tree, xdg_toplevel->base);
+ toplevel->scene_tree->node.data = toplevel;
+ // Popups look this up to find their parent's scene tree.
+ xdg_toplevel->base->data = toplevel->scene_tree;
+ toplevels.emplace(xdg_toplevel, std::move(owned));
+
+ toplevel->map.connect(xdg_toplevel->base->surface->events.map, [this, toplevel](void*) {
+ mapped_toplevels.push_front(toplevel);
+ focus_toplevel(toplevel);
+ wlr_log(WLR_INFO, "toplevel mapped: %s",
+ toplevel->xdg_toplevel->title != nullptr ? toplevel->xdg_toplevel->title : "?");
+ });
+ toplevel->unmap.connect(xdg_toplevel->base->surface->events.unmap, [this, toplevel](void*) {
+ if (toplevel == grabbed_toplevel) {
+ reset_cursor_mode();
+ }
+ mapped_toplevels.remove(toplevel);
+ });
+ toplevel->commit.connect(xdg_toplevel->base->surface->events.commit, [toplevel](void*) {
+ if (toplevel->xdg_toplevel->base->initial_commit) {
+ // Reply to the initial commit with a 0x0 configure: the client
+ // picks its own dimensions.
+ wlr_xdg_toplevel_set_size(toplevel->xdg_toplevel, 0, 0);
+ }
+ });
+ toplevel->destroy.connect(xdg_toplevel->events.destroy, [this, toplevel](void*) {
+ // Last action: destroys `toplevel` (and these listeners with it).
+ toplevels.erase(toplevel->xdg_toplevel);
+ });
+
+ toplevel->request_move.connect(xdg_toplevel->events.request_move, [this, toplevel](void*) {
+ begin_interactive(toplevel, CursorMode::Move, 0);
+ });
+ toplevel->request_resize.connect(
+ xdg_toplevel->events.request_resize, [this, toplevel](void* data) {
+ const auto* event = static_cast<wlr_xdg_toplevel_resize_event*>(data);
+ begin_interactive(toplevel, CursorMode::Resize, event->edges);
+ });
+ toplevel->request_maximize.connect(
+ xdg_toplevel->events.request_maximize, [toplevel](void*) {
+ // Unsupported, but xdg-shell demands a configure reply.
+ if (toplevel->xdg_toplevel->base->initialized) {
+ wlr_xdg_surface_schedule_configure(toplevel->xdg_toplevel->base);
+ }
+ });
+ toplevel->request_fullscreen.connect(
+ xdg_toplevel->events.request_fullscreen, [toplevel](void*) {
+ if (toplevel->xdg_toplevel->base->initialized) {
+ wlr_xdg_surface_schedule_configure(toplevel->xdg_toplevel->base);
+ }
+ });
+}
+
+// ---- xdg-shell popups ----------------------------------------------------------
+
+void Server::Impl::handle_new_popup(wlr_xdg_popup* xdg_popup) {
+ auto owned = std::make_unique<Popup>();
+ Popup* popup = owned.get();
+ popup->xdg_popup = xdg_popup;
+ popups.emplace(xdg_popup, std::move(owned));
+
+ // Parent's scene tree was stashed in base->data when it was created
+ // (toplevel or ancestor popup).
+ wlr_xdg_surface* parent = wlr_xdg_surface_try_from_wlr_surface(xdg_popup->parent);
+ if (parent != nullptr && parent->data != nullptr) {
+ auto* parent_tree = static_cast<wlr_scene_tree*>(parent->data);
+ xdg_popup->base->data = wlr_scene_xdg_surface_create(parent_tree, xdg_popup->base);
+ }
+
+ popup->commit.connect(xdg_popup->base->surface->events.commit, [popup](void*) {
+ if (popup->xdg_popup->base->initial_commit) {
+ // A fuller compositor would also unconstrain the popup to keep
+ // it on-screen here.
+ wlr_xdg_surface_schedule_configure(popup->xdg_popup->base);
+ }
+ });
+ popup->destroy.connect(xdg_popup->events.destroy, [this, popup](void*) {
+ // Last action: destroys `popup` (and these listeners with it).
+ popups.erase(popup->xdg_popup);
+ });
+}
+
+} // namespace unbox::kernel