From 46fe232306a5ebe09cb85b4a9a46f36ba442cba5 Mon Sep 17 00:00:00 2001 From: AstralZX <208268648+AstralZX@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:31:18 +0200 Subject: [PATCH] Add testWM: a minimal tiling WM built on wlroots++ Master/stack tiling with Alt as modifier: Alt+Enter spawns a terminal (kitty), Alt+Q closes, Alt+J/Alt+K cycle focus, click-to-focus. Reads ~/.config/testwm/testwm.conf for terminal/modifier/layout. Verified headless: two clients receive 640x720 master + 640x720 stack configures. Wrapper additions: Cursor motion/button/axis/frame signal hooks, Seat::keyboard_notify_enter, Toplevel::close(). --- examples/CMakeLists.txt | 3 + examples/testwm.cpp | 333 ++++++++++++++++++++++++++++++++++++++ include/wlr/seat.hpp | 14 ++ include/wlr/xdg_shell.hpp | 1 + 4 files changed, 351 insertions(+) create mode 100644 examples/testwm.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9450d60..ffaa356 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,3 +3,6 @@ target_link_libraries(scene_demo PRIVATE wlrootspp) add_executable(tinywl tinywl.cpp) target_link_libraries(tinywl PRIVATE wlrootspp) + +add_executable(testWM testwm.cpp) +target_link_libraries(testWM PRIVATE wlrootspp) diff --git a/examples/testwm.cpp b/examples/testwm.cpp new file mode 100644 index 0000000..be42f26 --- /dev/null +++ b/examples/testwm.cpp @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "wlr/display.hpp" +#include "wlr/backend.hpp" +#include "wlr/compositor.hpp" +#include "wlr/output.hpp" +#include "wlr/xdg_shell.hpp" +#include "wlr/seat.hpp" +#include "wlr/scene.hpp" +#include "wlr/node.hpp" + +struct ToplevelState { + wlr::XdgSurface xdg; + wlr::Tree tree; + bool mapped = false; + int gx = 0, gy = 0, gw = 0, gh = 0; + std::string title; +}; + +struct Server { + wlr::Display display; + wlr::Backend backend{display}; + wlr::Renderer renderer{wlr_renderer_autocreate(backend.raw())}; + wlr::Allocator allocator{wlr_allocator_autocreate(backend.raw(), renderer.raw())}; + wlr::Compositor compositor{display, renderer}; + wlr::XdgShell xdg_shell{display}; + wlr::Seat seat{display, "seat0"}; + wlr::Cursor cursor; + wlr::OutputLayout layout{display}; + wlr::Scene scene; + wlr::Tree root{wlr_scene_tree_create(scene.root())}; + + std::vector outputs; + std::vector toplevels; + ToplevelState* focused = nullptr; + struct wlr_keyboard* keyboard = nullptr; + wlr::SignalSink key_sink; + wl_event_source* scheduler_source = nullptr; + + uint32_t mod = WLR_MODIFIER_ALT; + std::string terminal = "kitty"; + int caps = 0; +}; + +static Server* S = nullptr; + +static void relayout() { + struct wlr_box b = S->layout.box(); + int n = 0; + for (auto* t : S->toplevels) + if (t->mapped) n++; + if (n == 0) + return; + if (n == 1) { + for (auto* t : S->toplevels) { + if (!t->mapped) continue; + t->gx = b.x; t->gy = b.y; t->gw = b.width; t->gh = b.height; + t->tree.set_position(b.x, b.y); + wlr::Toplevel(t->xdg.toplevel()).set_size(b.width, b.height); + } + return; + } + int mw = b.width / 2; + int i = 0; + for (auto* t : S->toplevels) { + if (!t->mapped) continue; + if (i == 0) { + t->gx = b.x; t->gy = b.y; t->gw = mw; t->gh = b.height; + t->tree.set_position(b.x, b.y); + wlr::Toplevel(t->xdg.toplevel()).set_size(mw, b.height); + } else { + int sh = b.height / (n - 1); + t->gx = b.x + mw; t->gy = b.y + (i - 1) * sh; t->gw = b.width - mw; t->gh = sh; + t->tree.set_position(t->gx, t->gy); + wlr::Toplevel(t->xdg.toplevel()).set_size(b.width - mw, sh); + } + i++; + } +} + +static void focus(ToplevelState* t) { + if (S->focused == t) + return; + if (S->focused) + wlr::Toplevel(S->focused->xdg.toplevel()).set_activated(false); + S->focused = t; + if (!t) { + S->seat.keyboard_notify_enter(nullptr); + return; + } + wlr::Toplevel(t->xdg.toplevel()).set_activated(true); + t->tree.raise_to_top(); + S->seat.keyboard_notify_enter(t->xdg.surface()); +} + +static void cycle_focus(int dir) { + std::vector vis; + for (auto* t : S->toplevels) + if (t->mapped) vis.push_back(t); + if (vis.empty()) + return; + int idx = 0; + for (size_t i = 0; i < vis.size(); i++) + if (vis[i] == S->focused) idx = (int)i; + idx = (idx + dir + (int)vis.size()) % (int)vis.size(); + focus(vis[idx]); +} + +static void spawn() { + pid_t p = fork(); + if (p == 0) { + execlp(S->terminal.c_str(), S->terminal.c_str(), nullptr); + _exit(127); + } +} + +static void on_key(void* data) { + auto* ev = static_cast(data); + if (ev->state != WL_KEYBOARD_KEY_STATE_PRESSED) { + if (S->focused) + S->seat.keyboard_notify_key(ev->time_msec, ev->keycode, ev->state); + return; + } + struct wlr_keyboard* kb = S->keyboard; + if (!kb) { + if (S->focused) + S->seat.keyboard_notify_key(ev->time_msec, ev->keycode, ev->state); + return; + } + uint32_t mods = wlr_keyboard_get_modifiers(kb); + xkb_keysym_t sym = xkb_state_key_get_one_sym(kb->xkb_state, ev->keycode + 8); + if (mods & S->mod) { + if (sym == XKB_KEY_Return) { spawn(); return; } + if (sym == XKB_KEY_q) { if (S->focused) wlr::Toplevel(S->focused->xdg.toplevel()).close(); return; } + if (sym == XKB_KEY_j) { cycle_focus(1); return; } + if (sym == XKB_KEY_k) { cycle_focus(-1); return; } + if (sym == XKB_KEY_Tab) { cycle_focus(1); return; } + } + if (S->focused) + S->seat.keyboard_notify_key(ev->time_msec, ev->keycode, ev->state); +} + +static void on_motion(void* data) { + auto* ev = static_cast(data); + S->seat.pointer_notify_motion(ev->time_msec, S->cursor.raw()->x, S->cursor.raw()->y); +} + +static void on_button(void* data) { + auto* ev = static_cast(data); + S->seat.pointer_notify_button(ev->time_msec, ev->button, ev->state); + if (ev->state == WL_POINTER_BUTTON_STATE_PRESSED) { + double cx = S->cursor.raw()->x, cy = S->cursor.raw()->y; + for (int i = (int)S->toplevels.size() - 1; i >= 0; i--) { + ToplevelState* t = S->toplevels[i]; + if (t->mapped && cx >= t->gx && cx < t->gx + t->gw && + cy >= t->gy && cy < t->gy + t->gh) { + focus(t); + break; + } + } + } +} + +static void on_axis(void* data) { + auto* ev = static_cast(data); + S->seat.pointer_notify_axis(ev->time_msec, ev->orientation, ev->delta, ev->delta_discrete, ev->source); +} + +static void on_frame(void*) { + S->seat.pointer_notify_frame(); +} + +static void on_new_toplevel(void* data) { + auto* t = static_cast(data); + ToplevelState* tn = new ToplevelState{ + wlr::XdgSurface{t->base}, + wlr::Tree(wlr_scene_xdg_surface_create(S->root.raw(), t->base)), + }; + S->toplevels.push_back(tn); + + tn->xdg.on_map([tn](void*) { + tn->mapped = true; + relayout(); + focus(tn); + }); + tn->xdg.on_unmap([tn](void*) { + tn->mapped = false; + relayout(); + if (S->focused == tn) + cycle_focus(1); + }); + tn->xdg.on_destroy([tn](void*) { + auto it = std::find(S->toplevels.begin(), S->toplevels.end(), tn); + if (it != S->toplevels.end()) + S->toplevels.erase(it); + if (S->focused == tn) + S->focused = nullptr; + if (tn->tree) + wlr_scene_node_destroy(&tn->tree.raw()->node); + delete tn; + relayout(); + }); + + wlr::Toplevel(t).on_set_title([tn](void* d) { + const char* s = static_cast(d)->title; + tn->title = s ? s : ""; + }); + + wl_event_loop_add_idle(S->display.event_loop(), + [](void* d) { + auto* s = static_cast(d); + if (s->initialized) + wlr_xdg_surface_schedule_configure(s); + }, t->base); +} + +static void on_new_output(void* data) { + auto* o = static_cast(data); + wlr::Output out(o); + out.set_preferred_mode(); + wlr_output_init_render(o, S->allocator.raw(), S->renderer.raw()); + wlr_output_create_global(o, S->display.raw()); + struct wlr_scene_output* so = wlr_scene_output_create(S->scene.raw(), o); + wlr_scene_output_set_position(so, 0, 0); + S->outputs.push_back(so); + S->layout.add_auto_output(o); + wl_event_source_timer_update(S->scheduler_source, 0); +} + +static void on_new_input(void* data) { + auto* dev = static_cast(data); + S->cursor.attach_input_device(dev); + if (dev->type == WLR_INPUT_DEVICE_KEYBOARD) { + struct wlr_keyboard* kb = wlr_keyboard_from_input_device(dev); + wlr::Keyboard k(kb); + k.set_default_keymap(); + S->keyboard = kb; + S->seat.set_keyboard(k); + S->key_sink.connect(&kb->events.key, on_key); + S->caps |= WL_SEAT_CAPABILITY_KEYBOARD; + } else if (dev->type == WLR_INPUT_DEVICE_POINTER) { + S->caps |= WL_SEAT_CAPABILITY_POINTER; + } + S->seat.set_capabilities((uint32_t)S->caps); +} + +static void commit_outputs() { + for (struct wlr_scene_output* so : S->outputs) + wlr_scene_output_commit(so, nullptr); +} + +static int frame_tick(void* data) { + (void)data; + commit_outputs(); + wl_event_source_timer_update(S->scheduler_source, 16); + return 0; +} + +static void load_config() { + const char* home = getenv("HOME"); + std::string path = std::string(home ? home : "") + "/.config/testwm/testwm.conf"; + if (getenv("TESTWM_CONFIG")) + path = getenv("TESTWM_CONFIG"); + std::ifstream f(path); + if (!f) + return; + std::string line; + while (std::getline(f, line)) { + line.erase(0, line.find_first_not_of(" \t")); + if (line.empty() || line[0] == '#') + continue; + auto eq = line.find('='); + if (eq == std::string::npos) + continue; + std::string k = line.substr(0, eq), v = line.substr(eq + 1); + k.erase(k.find_last_not_of(" \t") + 1); + v.erase(0, v.find_first_not_of(" \t")); + v.erase(v.find_last_not_of(" \t") + 1); + if (k == "terminal") S->terminal = v; + else if (k == "modifier") S->mod = (v == "super" || v == "win" || v == "logo") ? WLR_MODIFIER_LOGO : WLR_MODIFIER_ALT; + } +} + +int main() { + Server server; + S = &server; + + load_config(); + + if (!S->renderer.init_wl_display(S->display)) + return 1; + + S->cursor.attach_output_layout(S->layout.raw()); + S->cursor.on_motion(on_motion); + S->cursor.on_button(on_button); + S->cursor.on_axis(on_axis); + S->cursor.on_frame(on_frame); + + S->backend.on_new_output(on_new_output); + S->backend.on_new_input(on_new_input); + S->xdg_shell.on_new_toplevel(on_new_toplevel); + + S->scheduler_source = wl_event_loop_add_timer(S->display.event_loop(), frame_tick, nullptr); + + const char* socket = S->display.add_socket_auto(); + if (!socket) + return 1; + fprintf(stderr, + "testWM: WAYLAND_DISPLAY=%s\n" + "testWM: bindings: Alt+Enter spawn '%s', Alt+Q close, Alt+J/Alt+K cycle focus, click to focus\n", + socket, S->terminal.c_str()); + fflush(stderr); + + if (!S->backend.start()) + return 1; + + S->display.run(); + return 0; +} diff --git a/include/wlr/seat.hpp b/include/wlr/seat.hpp index 4628b31..ebeeddd 100644 --- a/include/wlr/seat.hpp +++ b/include/wlr/seat.hpp @@ -27,8 +27,17 @@ public: } void warp(double x, double y) { wlr_cursor_warp(m_cursor, nullptr, x, y); } + void on_motion(std::function cb) { m_motion.connect(&m_cursor->events.motion, std::move(cb)); } + void on_button(std::function cb) { m_button.connect(&m_cursor->events.button, std::move(cb)); } + void on_axis(std::function cb) { m_axis.connect(&m_cursor->events.axis, std::move(cb)); } + void on_frame(std::function cb) { m_frame.connect(&m_cursor->events.frame, std::move(cb)); } + private: struct wlr_cursor* m_cursor; + SignalSink m_motion; + SignalSink m_button; + SignalSink m_axis; + SignalSink m_frame; }; class Keyboard { @@ -71,6 +80,11 @@ public: void keyboard_notify_key(uint32_t time, uint32_t keycode, enum wl_keyboard_key_state state) { wlr_seat_keyboard_notify_key(m_seat, time, keycode, state); } + void keyboard_notify_enter(struct wlr_surface* surface, + uint32_t* keycodes = nullptr, size_t num_keycodes = 0, + struct wlr_keyboard_modifiers* modifiers = nullptr) { + wlr_seat_keyboard_notify_enter(m_seat, surface, keycodes, num_keycodes, modifiers); + } void pointer_notify_motion(uint32_t time, double sx, double sy) { wlr_seat_pointer_notify_motion(m_seat, time, sx, sy); } diff --git a/include/wlr/xdg_shell.hpp b/include/wlr/xdg_shell.hpp index af003d0..495cab0 100644 --- a/include/wlr/xdg_shell.hpp +++ b/include/wlr/xdg_shell.hpp @@ -32,6 +32,7 @@ public: uint32_t set_size(int w, int h) { return wlr_xdg_toplevel_set_size(m_toplevel, w, h); } uint32_t set_activated(bool activated) { return wlr_xdg_toplevel_set_activated(m_toplevel, activated); } uint32_t set_maximized(bool maximized) { return wlr_xdg_toplevel_set_maximized(m_toplevel, maximized); } + void close() { wlr_xdg_toplevel_send_close(m_toplevel); } void on_request_move(std::function cb) { m_requestMove.connect(&m_toplevel->events.request_move, std::move(cb));