diff options
Diffstat (limited to 'packages/kernel/src')
| -rw-r--r-- | packages/kernel/src/input.cpp | 316 | ||||
| -rw-r--r-- | packages/kernel/src/listener.hpp | 56 | ||||
| -rw-r--r-- | packages/kernel/src/server.cpp | 225 | ||||
| -rw-r--r-- | packages/kernel/src/server_impl.hpp | 162 | ||||
| -rw-r--r-- | packages/kernel/src/toplevel.cpp | 190 |
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 |
