diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 22:44:16 +0900 |
| commit | c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch) | |
| tree | f6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/kernel/src/input.cpp | |
| parent | 6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff) | |
| download | unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.tar.gz unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.zip | |
Slice 4: extension host + typed bus; xdg-shell/layer-shell extracted to core extensions
The kernel now names NO concrete feature. It owns: the extension host
(install/topological activate, missing-dep/cycle = startup error), the
typed Event/Filter bus (error-isolated: a throwing extension is disabled,
never the session; RAII Subscriptions), the Host API (per-extension
facade: borrows, scene layers, event catalogue, typed services), the
public RAII Listener, and a typed surface→scene-tree registry
(Host::host_surface/scene_tree_for) that replaced the untyped
wlr_surface.data convention both extensions flagged.
- ext-xdg-shell (core): toplevel/popup lifecycle, focus-on-map,
click/tap-to-focus, pointer/touch routing incl. button+axis (the
kernel only moves the cursor and emits — a contract-doc lie caught by
user hands-on), interactive move/resize via pure GrabMachine (fixes
the request-arrives-after-release race: grab requires request ∧
button-down, release always ends it), Alt+F1 cycle,
Ctrl+Alt+Backspace terminate (labwc's default A-Escape=Exit killed
the dev session once — never again; see nested-run skill).
- ext-layer-shell (core): wlr-layer-shell v1 (proto v5) for external
clients; pure doctest-hard arrangement core; fuzzel verified visually
nested (fix: seed outputs from output_layout at activate — events-only
tracking missed pre-activation outputs; plus a scene-node double-free).
- First protocol codegen: vendored wlr-layer-shell XML + wayland-scanner
server-header propagated through kernel_dep; wlr.hpp grew a
namespace→_namespace keyword fix for the generated header.
- Glossary: 'scene layer' (user-approved). New rules earned:
parallel-wave-builds, contract-docs.
- User hands-on verified: typing, click-to-focus, drag-select, scroll,
titlebar drag-move (slow + flick), Alt+F1, fuzzel + arrows, touch tap,
Ctrl+Alt+Backspace. 68 doctest cases green, ASan/UBSan clean (our
code), idle RSS ≈73 MiB.
Diffstat (limited to 'packages/kernel/src/input.cpp')
| -rw-r--r-- | packages/kernel/src/input.cpp | 241 |
1 files changed, 75 insertions, 166 deletions
diff --git a/packages/kernel/src/input.cpp b/packages/kernel/src/input.cpp index 7095154..b6c4c0c 100644 --- a/packages/kernel/src/input.cpp +++ b/packages/kernel/src/input.cpp @@ -4,6 +4,14 @@ namespace unbox::kernel { +// Slice 4: the kernel owns generic input PLUMBING only. It moves the cursor, +// tracks seat capabilities, handles the seat's own protocol requests, runs the +// key_filter (consume-or-pass), and EMITS typed events. It routes NOTHING to +// client surfaces and makes NO focus decision — ext-xdg-shell (and others) do +// that from the bus + borrows. The only client forward the kernel still does +// is keyboard key passthrough AFTER the filter, because the seat already holds +// the focus an extension set via wlr_seat_keyboard_notify_enter. + // ---- Device hotplug ----------------------------------------------------------- void Server::Impl::handle_new_input(wlr_input_device* device) { @@ -44,7 +52,7 @@ void Server::Impl::new_keyboard(wlr_input_device* device) { keyboard->keyboard = wlr_kb; keyboards.push_back(std::move(owned)); - // Default XKB keymap (layout "us" etc.); unbox.toml takes over later. + // Default XKB keymap; per-config keymap is a later slice. 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); @@ -53,7 +61,6 @@ void Server::Impl::new_keyboard(wlr_input_device* device) { 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); }); @@ -65,16 +72,39 @@ void Server::Impl::new_keyboard(wlr_input_device* device) { 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) { + const bool pressed = event->state == WL_KEYBOARD_KEY_STATE_PRESSED; + + // Thread each resolved keysym through the key_filter; a filter link may + // CONSUME the key (set handled=true) — that is how extensions implement + // compositor shortcuts. If any resolution is consumed, suppress the + // client forward for this key. + bool consumed = false; + for (int i = 0; i < nsyms; ++i) { + KeyEvent ke{}; + ke.keysym = syms[i]; + ke.keycode = event->keycode; + ke.modifiers = modifiers; + ke.pressed = pressed; + ke.time_msec = event->time_msec; + ke.handled = false; + ke = key_filter.apply(ke); + consumed = consumed || ke.handled; + } + // With no resolved syms (e.g. modifier-only) the filter still runs once + // so extensions can observe raw modifier keys if they wish. + if (nsyms == 0) { + KeyEvent ke{}; + ke.keysym = XKB_KEY_NoSymbol; + ke.keycode = event->keycode; + ke.modifiers = modifiers; + ke.pressed = pressed; + ke.time_msec = event->time_msec; + ke = key_filter.apply(ke); + consumed = consumed || ke.handled; + } + + if (!consumed) { wlr_seat_set_keyboard(seat, keyboard->keyboard); wlr_seat_keyboard_notify_key(seat, event->time_msec, event->keycode, event->state); } @@ -89,8 +119,8 @@ void Server::Impl::new_keyboard(wlr_input_device* device) { } 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. + // All pointer handling is proxied through wlr_cursor; per-device libinput + // config is a later slice. wlr_cursor_attach_input_device(cursor, device); } @@ -107,225 +137,104 @@ void Server::Impl::new_touch(wlr_input_device* device) { 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): move cursor + emit, route nothing ------------ -// ---- 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; - } - - // Slice-3 spike input proof (NOT the slice-5 routing contract): if the - // cursor is over the spike node, forward surface-local coords to RmlUi so - // the document's button reacts to hover. Crude and private. +void Server::Impl::emit_pointer_motion(std::uint32_t time_msec) { + // Slice-3 spike input proof (kernel-internal; NOT a contract): forward + // surface-local coords over the spike node so its button hovers. if (ui_spike != nullptr) { if (wlr_scene_node* spike = ui_spike->node()) { int nx = 0; int ny = 0; wlr_scene_node_coords(spike, &nx, &ny); - const double sx = cursor->x - nx; - const double sy = cursor->y - ny; - ui_spike->on_pointer_motion(sx, sy); + ui_spike->on_pointer_motion(cursor->x - nx, cursor->y - ny); } } - 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); - } + const PointerMotionEvent ev{cursor->x, cursor->y, time_msec}; + ev_pointer_motion.emit(ev); } 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); + emit_pointer_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); + emit_pointer_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); + const bool pressed = event->state == WL_POINTER_BUTTON_STATE_PRESSED; - // Slice-3 spike input proof: forward clicks over the spike node to - // RmlUi so its button reacts to press/release. Crude and private. + // Slice-3 spike input proof (kernel-internal): forward clicks over the + // spike node so its button reacts. if (ui_spike != nullptr) { if (wlr_scene_node* spike = ui_spike->node()) { - int nx = 0; - int ny = 0; - wlr_scene_node_coords(spike, &nx, &ny); if (wlr_scene_node_at(spike, cursor->x, cursor->y, nullptr, nullptr) != nullptr) { - ui_spike->on_pointer_button(event->state == - WL_POINTER_BUTTON_STATE_PRESSED); + ui_spike->on_pointer_button(pressed); } } } - 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)); - } + const PointerButtonEvent ev{event->button, pressed, cursor->x, cursor->y, + event->time_msec}; + ev_pointer_button.emit(ev); }); 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); + const PointerAxisEvent ev{event->orientation, event->delta, event->delta_discrete, + event->source, event->time_msec}; + ev_pointer_axis.emit(ev); }); + cursor_frame.connect(cursor->events.frame, [this](void*) { ev_pointer_frame.emit(); }); - // ---- Touch (tinywl doesn't have this; the CF-AX3 does) ---- + // ---- Touch (tinywl lacks this; the CF-AX3 has it). Convert to layout + // coords + emit; extensions route to surfaces. ---- 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); - } + const TouchDownEvent ev{event->touch_id, lx, ly, event->time_msec}; + ev_touch_down.emit(ev); }); 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); + const TouchMotionEvent ev{event->touch_id, lx, ly, event->time_msec}; + ev_touch_motion.emit(ev); }); 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); + const TouchUpEvent ev{event->touch_id, event->time_msec}; + ev_touch_up.emit(ev); }); 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); + const TouchCancelEvent ev{event->touch_id}; + ev_touch_cancel.emit(ev); }); cursor_touch_frame.connect(cursor->events.touch_frame, [this](void*) { - wlr_seat_touch_notify_frame(seat); + ev_touch_frame.emit(); }); } -// ---- Seat requests ------------------------------------------------------------- +// ---- Seat requests (generic protocol glue) ---------------------------------- 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); } |
