summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/kernel/src')
-rw-r--r--packages/kernel/src/input.cpp316
-rw-r--r--packages/kernel/src/listener.hpp56
-rw-r--r--packages/kernel/src/server.cpp225
-rw-r--r--packages/kernel/src/server_impl.hpp162
-rw-r--r--packages/kernel/src/toplevel.cpp190
5 files changed, 949 insertions, 0 deletions
diff --git a/packages/kernel/src/input.cpp b/packages/kernel/src/input.cpp
new file mode 100644
index 0000000..93bee74
--- /dev/null
+++ b/packages/kernel/src/input.cpp
@@ -0,0 +1,316 @@
+#include "server_impl.hpp"
+
+#include <xkbcommon/xkbcommon.h>
+
+namespace unbox::kernel {
+
+// ---- Device hotplug -----------------------------------------------------------
+
+void Server::Impl::handle_new_input(wlr_input_device* device) {
+ switch (device->type) {
+ case WLR_INPUT_DEVICE_KEYBOARD:
+ new_keyboard(device);
+ break;
+ case WLR_INPUT_DEVICE_POINTER:
+ new_pointer(device);
+ break;
+ case WLR_INPUT_DEVICE_TOUCH:
+ new_touch(device);
+ break;
+ default:
+ break;
+ }
+ update_seat_capabilities();
+}
+
+void Server::Impl::update_seat_capabilities() {
+ // Always advertise a pointer: we always draw a cursor.
+ std::uint32_t caps = WL_SEAT_CAPABILITY_POINTER;
+ if (!keyboards.empty()) {
+ caps |= WL_SEAT_CAPABILITY_KEYBOARD;
+ }
+ if (!touch_devices.empty()) {
+ caps |= WL_SEAT_CAPABILITY_TOUCH;
+ }
+ wlr_seat_set_capabilities(seat, caps);
+}
+
+void Server::Impl::new_keyboard(wlr_input_device* device) {
+ wlr_keyboard* wlr_kb = wlr_keyboard_from_input_device(device);
+
+ auto owned = std::make_unique<Keyboard>();
+ Keyboard* keyboard = owned.get();
+ keyboard->server = this;
+ keyboard->keyboard = wlr_kb;
+ keyboards.push_back(std::move(owned));
+
+ // Default XKB keymap (layout "us" etc.); unbox.toml takes over later.
+ xkb_context* context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ xkb_keymap* keymap = xkb_keymap_new_from_names(context, nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ wlr_keyboard_set_keymap(wlr_kb, keymap);
+ xkb_keymap_unref(keymap);
+ xkb_context_unref(context);
+ wlr_keyboard_set_repeat_info(wlr_kb, 25, 600);
+
+ keyboard->modifiers.connect(wlr_kb->events.modifiers, [this, keyboard](void*) {
+ // The seat exposes one logical keyboard; swap the active device in.
+ wlr_seat_set_keyboard(seat, keyboard->keyboard);
+ wlr_seat_keyboard_notify_modifiers(seat, &keyboard->keyboard->modifiers);
+ });
+ keyboard->key.connect(wlr_kb->events.key, [this, keyboard](void* data) {
+ const auto* event = static_cast<wlr_keyboard_key_event*>(data);
+
+ // libinput keycode -> xkbcommon
+ const std::uint32_t keycode = event->keycode + 8;
+ const xkb_keysym_t* syms = nullptr;
+ const int nsyms =
+ xkb_state_key_get_syms(keyboard->keyboard->xkb_state, keycode, &syms);
+
+ bool handled = false;
+ const std::uint32_t modifiers = wlr_keyboard_get_modifiers(keyboard->keyboard);
+ if ((modifiers & WLR_MODIFIER_ALT) != 0 &&
+ event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
+ for (int i = 0; i < nsyms; i++) {
+ handled = handle_keybinding(syms[i]);
+ }
+ }
+ if (!handled) {
+ wlr_seat_set_keyboard(seat, keyboard->keyboard);
+ wlr_seat_keyboard_notify_key(seat, event->time_msec, event->keycode, event->state);
+ }
+ });
+ keyboard->destroy.connect(device->events.destroy, [this, keyboard](void*) {
+ update_seat_capabilities();
+ // Last action: destroys `keyboard` (and these listeners with it).
+ keyboards.remove_if([keyboard](const auto& owned) { return owned.get() == keyboard; });
+ });
+
+ wlr_seat_set_keyboard(seat, wlr_kb);
+}
+
+void Server::Impl::new_pointer(wlr_input_device* device) {
+ // All pointer handling is proxied through wlr_cursor; per-device
+ // libinput config (acceleration, tap…) is a later slice.
+ wlr_cursor_attach_input_device(cursor, device);
+}
+
+void Server::Impl::new_touch(wlr_input_device* device) {
+ auto owned = std::make_unique<TouchDevice>();
+ TouchDevice* touch = owned.get();
+ touch->server = this;
+ touch->device = device;
+ touch_devices.push_back(std::move(owned));
+
+ touch->destroy.connect(device->events.destroy, [this, touch](void*) {
+ update_seat_capabilities();
+ // Last action: destroys `touch` (and this listener with it).
+ touch_devices.remove_if([touch](const auto& owned) { return owned.get() == touch; });
+ });
+
+ // wlr_cursor aggregates touch devices too and emits layout-mapped
+ // touch_* events (handled below).
+ wlr_cursor_attach_input_device(cursor, device);
+}
+
+// ---- Compositor keybindings ------------------------------------------------------
+
+auto Server::Impl::handle_keybinding(std::uint32_t keysym) -> bool {
+ // Slice-2 placeholder bindings (Alt held), replaced by the keybinding
+ // filter chain in slice 5.
+ switch (keysym) {
+ case XKB_KEY_Escape:
+ wl_display_terminate(display);
+ return true;
+ case XKB_KEY_F1:
+ if (mapped_toplevels.size() >= 2) {
+ focus_toplevel(mapped_toplevels.back());
+ }
+ return true;
+ default:
+ return false;
+ }
+}
+
+// ---- Pointer (via wlr_cursor) ------------------------------------------------------
+
+void Server::Impl::process_cursor_move() {
+ wlr_scene_node_set_position(&grabbed_toplevel->scene_tree->node,
+ static_cast<int>(cursor->x - grab_x),
+ static_cast<int>(cursor->y - grab_y));
+}
+
+void Server::Impl::process_cursor_resize() {
+ // Resizing moves the node when dragging top/left edges; the client is
+ // asked for the new size (it commits a matching buffer later).
+ Toplevel* toplevel = grabbed_toplevel;
+ const double border_x = cursor->x - grab_x;
+ const double border_y = cursor->y - grab_y;
+ int new_left = grab_geobox.x;
+ int new_right = grab_geobox.x + grab_geobox.width;
+ int new_top = grab_geobox.y;
+ int new_bottom = grab_geobox.y + grab_geobox.height;
+
+ if ((resize_edges & WLR_EDGE_TOP) != 0) {
+ new_top = static_cast<int>(border_y);
+ if (new_top >= new_bottom) {
+ new_top = new_bottom - 1;
+ }
+ } else if ((resize_edges & WLR_EDGE_BOTTOM) != 0) {
+ new_bottom = static_cast<int>(border_y);
+ if (new_bottom <= new_top) {
+ new_bottom = new_top + 1;
+ }
+ }
+ if ((resize_edges & WLR_EDGE_LEFT) != 0) {
+ new_left = static_cast<int>(border_x);
+ if (new_left >= new_right) {
+ new_left = new_right - 1;
+ }
+ } else if ((resize_edges & WLR_EDGE_RIGHT) != 0) {
+ new_right = static_cast<int>(border_x);
+ if (new_right <= new_left) {
+ new_right = new_left + 1;
+ }
+ }
+
+ wlr_box* geo_box = &toplevel->xdg_toplevel->base->geometry;
+ wlr_scene_node_set_position(&toplevel->scene_tree->node, new_left - geo_box->x,
+ new_top - geo_box->y);
+ wlr_xdg_toplevel_set_size(toplevel->xdg_toplevel, new_right - new_left,
+ new_bottom - new_top);
+}
+
+void Server::Impl::process_cursor_motion(std::uint32_t time_msec) {
+ if (cursor_mode == CursorMode::Move) {
+ process_cursor_move();
+ return;
+ }
+ if (cursor_mode == CursorMode::Resize) {
+ process_cursor_resize();
+ return;
+ }
+
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ Toplevel* toplevel = toplevel_at(cursor->x, cursor->y, &surface, &sx, &sy);
+ if (toplevel == nullptr) {
+ // Over no toplevel: the compositor draws its own default cursor.
+ wlr_cursor_set_xcursor(cursor, cursor_mgr, "default");
+ }
+ if (surface != nullptr) {
+ // Enter gives the surface pointer focus; wlroots dedupes repeats.
+ wlr_seat_pointer_notify_enter(seat, surface, sx, sy);
+ wlr_seat_pointer_notify_motion(seat, time_msec, sx, sy);
+ } else {
+ wlr_seat_pointer_clear_focus(seat);
+ }
+}
+
+void Server::Impl::attach_cursor_handlers() {
+ cursor_motion.connect(cursor->events.motion, [this](void* data) {
+ const auto* event = static_cast<wlr_pointer_motion_event*>(data);
+ wlr_cursor_move(cursor, &event->pointer->base, event->delta_x, event->delta_y);
+ process_cursor_motion(event->time_msec);
+ });
+ cursor_motion_absolute.connect(cursor->events.motion_absolute, [this](void* data) {
+ const auto* event = static_cast<wlr_pointer_motion_absolute_event*>(data);
+ wlr_cursor_warp_absolute(cursor, &event->pointer->base, event->x, event->y);
+ process_cursor_motion(event->time_msec);
+ });
+ cursor_button.connect(cursor->events.button, [this](void* data) {
+ const auto* event = static_cast<wlr_pointer_button_event*>(data);
+ wlr_seat_pointer_notify_button(seat, event->time_msec, event->button, event->state);
+ if (event->state == WL_POINTER_BUTTON_STATE_RELEASED) {
+ reset_cursor_mode();
+ } else {
+ // Click-to-focus.
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ focus_toplevel(toplevel_at(cursor->x, cursor->y, &surface, &sx, &sy));
+ }
+ });
+ cursor_axis.connect(cursor->events.axis, [this](void* data) {
+ const auto* event = static_cast<wlr_pointer_axis_event*>(data);
+ wlr_seat_pointer_notify_axis(seat, event->time_msec, event->orientation, event->delta,
+ event->delta_discrete, event->source,
+ event->relative_direction);
+ });
+ cursor_frame.connect(cursor->events.frame, [this](void*) {
+ wlr_seat_pointer_notify_frame(seat);
+ });
+
+ // ---- Touch (tinywl doesn't have this; the CF-AX3 does) ----
+ cursor_touch_down.connect(cursor->events.touch_down, [this](void* data) {
+ const auto* event = static_cast<wlr_touch_down_event*>(data);
+ double lx = 0;
+ double ly = 0;
+ wlr_cursor_absolute_to_layout_coords(cursor, &event->touch->base, event->x, event->y,
+ &lx, &ly);
+ double sx = 0;
+ double sy = 0;
+ wlr_surface* surface = nullptr;
+ Toplevel* toplevel = toplevel_at(lx, ly, &surface, &sx, &sy);
+ if (toplevel != nullptr) {
+ focus_toplevel(toplevel); // tap raises + focuses
+ }
+ if (surface != nullptr) {
+ touch_points.insert_or_assign(event->touch_id,
+ TouchPoint{surface, lx - sx, ly - sy});
+ wlr_seat_touch_notify_down(seat, surface, event->time_msec, event->touch_id, sx, sy);
+ }
+ });
+ cursor_touch_motion.connect(cursor->events.touch_motion, [this](void* data) {
+ const auto* event = static_cast<wlr_touch_motion_event*>(data);
+ auto it = touch_points.find(event->touch_id);
+ if (it == touch_points.end()) {
+ return; // down landed on no surface; nothing is grabbed
+ }
+ double lx = 0;
+ double ly = 0;
+ wlr_cursor_absolute_to_layout_coords(cursor, &event->touch->base, event->x, event->y,
+ &lx, &ly);
+ wlr_seat_touch_notify_motion(seat, event->time_msec, event->touch_id,
+ lx - it->second.origin_x, ly - it->second.origin_y);
+ });
+ cursor_touch_up.connect(cursor->events.touch_up, [this](void* data) {
+ const auto* event = static_cast<wlr_touch_up_event*>(data);
+ touch_points.erase(event->touch_id);
+ wlr_seat_touch_notify_up(seat, event->time_msec, event->touch_id);
+ });
+ cursor_touch_cancel.connect(cursor->events.touch_cancel, [this](void* data) {
+ const auto* event = static_cast<wlr_touch_cancel_event*>(data);
+ if (wlr_touch_point* point = wlr_seat_touch_get_point(seat, event->touch_id)) {
+ wlr_seat_touch_notify_cancel(seat, point->client);
+ }
+ touch_points.erase(event->touch_id);
+ });
+ cursor_touch_frame.connect(cursor->events.touch_frame, [this](void*) {
+ wlr_seat_touch_notify_frame(seat);
+ });
+}
+
+// ---- Seat requests -------------------------------------------------------------
+
+void Server::Impl::attach_seat_handlers() {
+ seat_request_cursor.connect(seat->events.request_set_cursor, [this](void* data) {
+ const auto* event = static_cast<wlr_seat_pointer_request_set_cursor_event*>(data);
+ // Any client may send this; honor only the pointer-focused one.
+ if (seat->pointer_state.focused_client == event->seat_client) {
+ wlr_cursor_set_surface(cursor, event->surface, event->hotspot_x, event->hotspot_y);
+ }
+ });
+ seat_pointer_focus_change.connect(seat->pointer_state.events.focus_change, [this](void* data) {
+ const auto* event = static_cast<wlr_seat_pointer_focus_change_event*>(data);
+ if (event->new_surface == nullptr) {
+ wlr_cursor_set_xcursor(cursor, cursor_mgr, "default");
+ }
+ });
+ seat_request_set_selection.connect(seat->events.request_set_selection, [this](void* data) {
+ const auto* event = static_cast<wlr_seat_request_set_selection_event*>(data);
+ wlr_seat_set_selection(seat, event->source, event->serial);
+ });
+}
+
+} // namespace unbox::kernel
diff --git a/packages/kernel/src/listener.hpp b/packages/kernel/src/listener.hpp
new file mode 100644
index 0000000..5b39f74
--- /dev/null
+++ b/packages/kernel/src/listener.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <unbox/kernel/wlr.hpp>
+
+#include <functional>
+#include <utility>
+
+namespace unbox::kernel {
+
+// RAII wl_listener: connect() subscribes, destruction/disconnect()
+// unsubscribes. PRIVATE slice-2 helper — the public typed subscription
+// handle arrives with the bus in slice 4 (.unbox/rules/listener-lifetime.md).
+//
+// A handler MAY destroy its own Listener (the destroy-event pattern: a
+// handler erases its owning entity from a container). This is safe because
+// thunk() touches nothing after handler_() returns — but the handler itself
+// must not touch captures after triggering its own destruction; make the
+// erase/delete its LAST action.
+class Listener {
+public:
+ Listener() {
+ node_.self = this;
+ node_.listener.notify = &Listener::thunk;
+ wl_list_init(&node_.listener.link);
+ }
+ ~Listener() { disconnect(); }
+ Listener(const Listener&) = delete;
+ auto operator=(const Listener&) -> Listener& = delete;
+
+ void connect(wl_signal& signal, std::function<void(void*)> handler) {
+ disconnect();
+ handler_ = std::move(handler);
+ wl_signal_add(&signal, &node_.listener);
+ }
+
+ void disconnect() {
+ wl_list_remove(&node_.listener.link);
+ wl_list_init(&node_.listener.link);
+ }
+
+private:
+ struct Node {
+ wl_listener listener; // MUST stay first: thunk casts wl_listener* -> Node*
+ Listener* self;
+ };
+
+ static void thunk(wl_listener* listener, void* data) {
+ auto* node = reinterpret_cast<Node*>(listener);
+ node->self->handler_(data);
+ }
+
+ Node node_{};
+ std::function<void(void*)> handler_;
+};
+
+} // namespace unbox::kernel
diff --git a/packages/kernel/src/server.cpp b/packages/kernel/src/server.cpp
new file mode 100644
index 0000000..d4d6b56
--- /dev/null
+++ b/packages/kernel/src/server.cpp
@@ -0,0 +1,225 @@
+#include "server_impl.hpp"
+
+#include <ctime>
+#include <stdexcept>
+#include <unistd.h>
+
+namespace unbox::kernel {
+
+namespace {
+
+template <typename T>
+auto require(T* ptr, const char* what) -> T* {
+ if (ptr == nullptr) {
+ throw std::runtime_error(std::string("failed to create ") + what);
+ }
+ return ptr;
+}
+
+} // namespace
+
+// ---- Server (public surface) ----------------------------------------------
+
+auto Server::create(Options options) -> std::unique_ptr<Server> {
+ auto impl = std::make_unique<Impl>();
+ impl->options = std::move(options);
+ try {
+ impl->init();
+ } catch (...) {
+ impl->shutdown();
+ throw;
+ }
+ return std::unique_ptr<Server>(new Server(std::move(impl)));
+}
+
+Server::Server(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
+
+Server::~Server() {
+ impl_->shutdown();
+}
+
+auto Server::socket_name() const -> std::string {
+ return impl_->socket;
+}
+
+void Server::run() {
+ wlr_log(WLR_INFO, "unbox running on WAYLAND_DISPLAY=%s", impl_->socket.c_str());
+ wl_display_run(impl_->display);
+}
+
+auto Server::dispatch(int timeout_ms) -> bool {
+ wl_event_loop* loop = wl_display_get_event_loop(impl_->display);
+ const int rc = wl_event_loop_dispatch(loop, timeout_ms);
+ wl_display_flush_clients(impl_->display);
+ return rc >= 0;
+}
+
+void Server::terminate() {
+ wl_display_terminate(impl_->display);
+}
+
+// ---- Impl lifecycle --------------------------------------------------------
+
+void Server::Impl::init() {
+ wlr_log_init(WLR_INFO, nullptr);
+
+ display = require(wl_display_create(), "wl_display");
+ backend = require(wlr_backend_autocreate(wl_display_get_event_loop(display), nullptr),
+ "wlr_backend");
+ renderer = require(wlr_renderer_autocreate(backend), "wlr_renderer");
+ wlr_renderer_init_wl_display(renderer, display);
+ allocator = require(wlr_allocator_autocreate(backend, renderer), "wlr_allocator");
+
+ wlr_compositor_create(display, 5, renderer);
+ wlr_subcompositor_create(display);
+ wlr_data_device_manager_create(display);
+
+ output_layout = require(wlr_output_layout_create(display), "wlr_output_layout");
+ new_output.connect(backend->events.new_output, [this](void* data) {
+ handle_new_output(static_cast<wlr_output*>(data));
+ });
+
+ scene = require(wlr_scene_create(), "wlr_scene");
+ scene_layout = require(wlr_scene_attach_output_layout(scene, output_layout),
+ "wlr_scene_output_layout");
+
+ xdg_shell = require(wlr_xdg_shell_create(display, 3), "wlr_xdg_shell");
+ new_xdg_toplevel.connect(xdg_shell->events.new_toplevel, [this](void* data) {
+ handle_new_toplevel(static_cast<wlr_xdg_toplevel*>(data));
+ });
+ new_xdg_popup.connect(xdg_shell->events.new_popup, [this](void* data) {
+ handle_new_popup(static_cast<wlr_xdg_popup*>(data));
+ });
+
+ cursor = require(wlr_cursor_create(), "wlr_cursor");
+ wlr_cursor_attach_output_layout(cursor, output_layout);
+ cursor_mgr = require(wlr_xcursor_manager_create(nullptr, 24), "wlr_xcursor_manager");
+ attach_cursor_handlers();
+
+ new_input.connect(backend->events.new_input, [this](void* data) {
+ handle_new_input(static_cast<wlr_input_device*>(data));
+ });
+ seat = require(wlr_seat_create(display, "seat0"), "wlr_seat");
+ attach_seat_handlers();
+
+ const char* socket_cstr = wl_display_add_socket_auto(display);
+ if (socket_cstr == nullptr) {
+ throw std::runtime_error("failed to add a Wayland socket");
+ }
+ socket = socket_cstr;
+
+ if (!wlr_backend_start(backend)) {
+ throw std::runtime_error("failed to start the wlr_backend");
+ }
+
+ if (!options.startup_cmd.empty()) {
+ if (fork() == 0) {
+ // Child only: don't pollute our own environment.
+ setenv("WAYLAND_DISPLAY", socket.c_str(), 1);
+ execl("/bin/sh", "/bin/sh", "-c", options.startup_cmd.c_str(),
+ static_cast<char*>(nullptr));
+ _exit(127);
+ }
+ }
+}
+
+void Server::Impl::shutdown() {
+ if (display != nullptr) {
+ wl_display_destroy_clients(display); // fires toplevel/popup destroy events
+ }
+
+ // Server-level listeners must detach BEFORE the wlr objects owning their
+ // signals die; a wl_listener outliving its signal is a use-after-free.
+ new_output.disconnect();
+ new_input.disconnect();
+ new_xdg_toplevel.disconnect();
+ new_xdg_popup.disconnect();
+ cursor_motion.disconnect();
+ cursor_motion_absolute.disconnect();
+ cursor_button.disconnect();
+ cursor_axis.disconnect();
+ cursor_frame.disconnect();
+ cursor_touch_down.disconnect();
+ cursor_touch_up.disconnect();
+ cursor_touch_motion.disconnect();
+ cursor_touch_cancel.disconnect();
+ cursor_touch_frame.disconnect();
+ seat_request_cursor.disconnect();
+ seat_pointer_focus_change.disconnect();
+ seat_request_set_selection.disconnect();
+
+ if (scene != nullptr) {
+ wlr_scene_node_destroy(&scene->tree.node);
+ scene = nullptr;
+ }
+ if (cursor_mgr != nullptr) {
+ wlr_xcursor_manager_destroy(cursor_mgr);
+ cursor_mgr = nullptr;
+ }
+ if (cursor != nullptr) {
+ wlr_cursor_destroy(cursor);
+ cursor = nullptr;
+ }
+ if (allocator != nullptr) {
+ wlr_allocator_destroy(allocator);
+ allocator = nullptr;
+ }
+ if (renderer != nullptr) {
+ wlr_renderer_destroy(renderer);
+ renderer = nullptr;
+ }
+ if (backend != nullptr) {
+ wlr_backend_destroy(backend); // fires output + input-device destroy events
+ backend = nullptr;
+ }
+ if (display != nullptr) {
+ wl_display_destroy(display);
+ display = nullptr;
+ }
+}
+
+// ---- Outputs ----------------------------------------------------------------
+
+void Server::Impl::handle_new_output(wlr_output* wlr_output) {
+ wlr_output_init_render(wlr_output, allocator, renderer);
+
+ wlr_output_state state;
+ wlr_output_state_init(&state);
+ wlr_output_state_set_enabled(&state, true);
+ if (wlr_output_mode* mode = wlr_output_preferred_mode(wlr_output)) {
+ wlr_output_state_set_mode(&state, mode);
+ }
+ wlr_output_commit_state(wlr_output, &state);
+ wlr_output_state_finish(&state);
+
+ auto owned = std::make_unique<Output>();
+ Output* output = owned.get();
+ output->server = this;
+ output->output = wlr_output;
+ outputs.push_back(std::move(owned));
+
+ output->frame.connect(wlr_output->events.frame, [this, output](void*) {
+ wlr_scene_output* scene_output = wlr_scene_get_scene_output(scene, output->output);
+ wlr_scene_output_commit(scene_output, nullptr);
+
+ timespec now{};
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ wlr_scene_output_send_frame_done(scene_output, &now);
+ });
+ output->request_state.connect(wlr_output->events.request_state, [output](void* data) {
+ const auto* event = static_cast<wlr_output_event_request_state*>(data);
+ wlr_output_commit_state(output->output, event->state);
+ });
+ output->destroy.connect(wlr_output->events.destroy, [this, output](void*) {
+ // Last action: destroys `output` (and these listeners with it).
+ outputs.remove_if([output](const auto& owned) { return owned.get() == output; });
+ });
+
+ wlr_output_layout_output* layout_output = wlr_output_layout_add_auto(output_layout, wlr_output);
+ wlr_scene_output* scene_output = wlr_scene_output_create(scene, wlr_output);
+ wlr_scene_output_layout_add_output(scene_layout, layout_output, scene_output);
+
+ wlr_log(WLR_INFO, "new output %s", wlr_output->name);
+}
+
+} // namespace unbox::kernel
diff --git a/packages/kernel/src/server_impl.hpp b/packages/kernel/src/server_impl.hpp
new file mode 100644
index 0000000..3abf739
--- /dev/null
+++ b/packages/kernel/src/server_impl.hpp
@@ -0,0 +1,162 @@
+#pragma once
+
+#include <unbox/kernel/server.hpp>
+#include <unbox/kernel/wlr.hpp>
+
+#include "listener.hpp"
+
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+#include <unordered_map>
+
+// Private kernel state. Entity structs mirror tinywl's, with Listener
+// members replacing manual wl_list_remove bookkeeping (RAII unsubscribes).
+// Definitions are split: server.cpp (lifecycle + outputs), toplevel.cpp
+// (xdg-shell + focus + grabs), input.cpp (devices, cursor, touch, seat).
+
+namespace unbox::kernel {
+
+struct Toplevel;
+
+enum class CursorMode { Passthrough, Move, Resize };
+
+struct Output {
+ Server::Impl* server = nullptr;
+ wlr_output* output = nullptr;
+ Listener frame;
+ Listener request_state;
+ Listener destroy;
+};
+
+struct Toplevel {
+ Server::Impl* server = nullptr;
+ wlr_xdg_toplevel* xdg_toplevel = nullptr;
+ wlr_scene_tree* scene_tree = nullptr;
+ Listener map;
+ Listener unmap;
+ Listener commit;
+ Listener destroy;
+ Listener request_move;
+ Listener request_resize;
+ Listener request_maximize;
+ Listener request_fullscreen;
+};
+
+struct Popup {
+ wlr_xdg_popup* xdg_popup = nullptr;
+ Listener commit;
+ Listener destroy;
+};
+
+struct Keyboard {
+ Server::Impl* server = nullptr;
+ wlr_keyboard* keyboard = nullptr;
+ Listener modifiers;
+ Listener key;
+ Listener destroy;
+};
+
+struct TouchDevice {
+ Server::Impl* server = nullptr;
+ wlr_input_device* device = nullptr;
+ Listener destroy;
+};
+
+struct Server::Impl {
+ Options options;
+
+ wl_display* display = nullptr;
+ wlr_backend* backend = nullptr;
+ wlr_renderer* renderer = nullptr;
+ wlr_allocator* allocator = nullptr;
+ wlr_scene* scene = nullptr;
+ wlr_scene_output_layout* scene_layout = nullptr;
+ wlr_output_layout* output_layout = nullptr;
+ wlr_xdg_shell* xdg_shell = nullptr;
+ wlr_cursor* cursor = nullptr;
+ wlr_xcursor_manager* cursor_mgr = nullptr;
+ wlr_seat* seat = nullptr;
+ std::string socket;
+
+ // Ownership (RAII teardown); drained naturally during shutdown by the
+ // destroy events wl_display_destroy_clients / backend destroy fire.
+ std::list<std::unique_ptr<Output>> outputs;
+ std::unordered_map<wlr_xdg_toplevel*, std::unique_ptr<Toplevel>> toplevels;
+ std::unordered_map<wlr_xdg_popup*, std::unique_ptr<Popup>> popups;
+ std::list<std::unique_ptr<Keyboard>> keyboards;
+ std::list<std::unique_ptr<TouchDevice>> touch_devices;
+
+ // Focus order: front = focused. Contains MAPPED toplevels only.
+ std::list<Toplevel*> mapped_toplevels;
+
+ // Interactive move/resize grab state (one grab at a time).
+ CursorMode cursor_mode = CursorMode::Passthrough;
+ Toplevel* grabbed_toplevel = nullptr;
+ double grab_x = 0.0;
+ double grab_y = 0.0;
+ wlr_box grab_geobox{};
+ std::uint32_t resize_edges = 0;
+
+ // Touch: Wayland implicitly grabs a touch point to the surface that
+ // received down; we record the surface's layout origin at down time to
+ // derive surface-local coords for motion. Assumes the surface doesn't
+ // move mid-touch (true except during interactive grabs; slice 5 will
+ // route input properly).
+ struct TouchPoint {
+ wlr_surface* surface = nullptr;
+ double origin_x = 0.0;
+ double origin_y = 0.0;
+ };
+ std::unordered_map<std::int32_t, TouchPoint> touch_points;
+
+ // Server-level listeners (disconnected explicitly in shutdown() BEFORE
+ // the wlr objects owning their signals are destroyed).
+ Listener new_output;
+ Listener new_input;
+ Listener new_xdg_toplevel;
+ Listener new_xdg_popup;
+ Listener cursor_motion;
+ Listener cursor_motion_absolute;
+ Listener cursor_button;
+ Listener cursor_axis;
+ Listener cursor_frame;
+ Listener cursor_touch_down;
+ Listener cursor_touch_up;
+ Listener cursor_touch_motion;
+ Listener cursor_touch_cancel;
+ Listener cursor_touch_frame;
+ Listener seat_request_cursor;
+ Listener seat_pointer_focus_change;
+ Listener seat_request_set_selection;
+
+ // server.cpp
+ void init(); // throws std::runtime_error on any component failure
+ void shutdown();
+ void handle_new_output(wlr_output* output);
+
+ // toplevel.cpp
+ void handle_new_toplevel(wlr_xdg_toplevel* toplevel);
+ void handle_new_popup(wlr_xdg_popup* popup);
+ void focus_toplevel(Toplevel* toplevel);
+ auto toplevel_at(double lx, double ly, wlr_surface** surface, double* sx, double* sy)
+ -> Toplevel*;
+ void begin_interactive(Toplevel* toplevel, CursorMode mode, std::uint32_t edges);
+ void reset_cursor_mode();
+
+ // input.cpp
+ void handle_new_input(wlr_input_device* device);
+ void new_keyboard(wlr_input_device* device);
+ void new_pointer(wlr_input_device* device);
+ void new_touch(wlr_input_device* device);
+ void update_seat_capabilities();
+ auto handle_keybinding(std::uint32_t keysym) -> bool;
+ void attach_cursor_handlers();
+ void attach_seat_handlers();
+ void process_cursor_motion(std::uint32_t time_msec);
+ void process_cursor_move();
+ void process_cursor_resize();
+};
+
+} // namespace unbox::kernel
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