kick/tabbed-wayland.c

1821 lines
69 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <execinfo.h>
#include <wayland-server.h>
#include <wayland-server-protocol.h>
#include "xdg-shell-server.h"
#include <gtk/gtk.h>
#include <xkbcommon/xkbcommon.h>
#define MAX_APPS 8
static const char *default_apps[] = {
NULL /* no default apps */
};
struct tabbed_surface;
struct tabbed_compositor;
struct tabbed_pool {
int fd;
size_t size;
unsigned char *data;
};
struct tabbed_buffer {
struct wl_resource *resource;
struct tabbed_pool *pool;
int32_t offset;
int32_t width, height, stride;
uint32_t format;
};
struct tabbed_output {
struct wl_global *global;
struct wl_list resources;
int width, height;
unsigned char *data;
size_t stride;
};
struct tabbed_surface {
struct wl_resource *resource;
struct wl_list link;
struct tabbed_buffer *buffer;
struct tabbed_buffer *pending_release;
bool has_buffer;
struct wl_listener surface_destroy_listener;
struct wl_resource *xdg_surface;
struct wl_resource *xdg_toplevel;
char *title;
char *app_id;
bool configured;
uint32_t configure_serial;
struct wl_resource *frame_callback;
struct wl_client *client;
GtkWidget *tab_label;
int tab_index;
unsigned char *pixels;
int pixel_width, pixel_height, pixel_stride;
int geom_x, geom_y, geom_width, geom_height;
};
struct tabbed_app {
char *cmd;
pid_t pid;
GtkWidget *tab_label;
int tab_index;
struct tabbed_surface *surface;
};
struct tabbed_compositor {
struct wl_display *display;
struct wl_event_loop *loop;
struct wl_list surfaces;
struct tabbed_output output;
struct tabbed_surface *active_surface;
struct tabbed_app *apps[MAX_APPS];
int num_apps;
struct wl_list input_tracks;
GtkWidget *window;
GtkWidget *notebook;
GtkWidget *canvas;
uint32_t serial;
bool clients_launched;
bool resize_pending;
int resize_w, resize_h;
/* xkb */
struct xkb_context *xkb_ctx;
struct xkb_keymap *xkb_keymap;
struct xkb_state *xkb_state;
char *xkb_keymap_str;
int xkb_keymap_fd;
int xkb_keymap_size;
/* keyboard focus */
struct tabbed_surface *kbd_focus;
struct wl_resource *kbd_resource;
/* pointer tracking */
bool pointer_entered;
double pointer_x, pointer_y;
uint32_t input_serial;
};
static struct tabbed_compositor compositor;
/* Forward declarations for interface implementations */
static const struct wl_compositor_interface compositor_impl;
static const struct wl_surface_interface surface_impl;
static const struct wl_region_interface region_impl;
static const struct wl_shm_interface shm_impl;
static const struct wl_shm_pool_interface pool_impl;
static const struct xdg_wm_base_interface xdg_wm_base_impl;
static const struct xdg_surface_interface xdg_surface_impl;
static const struct xdg_toplevel_interface xdg_toplevel_impl;
static const struct xdg_popup_interface xdg_popup_impl;
static const struct wl_seat_interface seat_impl;
static const struct wl_subcompositor_interface subcompositor_impl;
static const struct wl_data_device_manager_interface data_device_manager_impl;
static const struct wl_pointer_interface pointer_impl;
static const struct wl_keyboard_interface keyboard_impl;
static const struct wl_touch_interface touch_impl;
static const struct wl_subsurface_interface subsurface_impl;
static const struct wl_seat_interface seat_impl;
static const struct wl_buffer_interface buffer_impl;
static const struct xdg_positioner_interface positioner_impl;
static void launch_client(const char *cmd, int tab_index);
/* surface destroy callback */
static void surface_destroyed_cb(struct wl_listener *listener, void *data) {
(void)data;
struct tabbed_surface *surface = wl_container_of(listener, surface, surface_destroy_listener);
wl_list_remove(&surface->link);
if (compositor.active_surface == surface)
compositor.active_surface = NULL;
if (surface->pending_release)
wl_buffer_send_release(surface->pending_release->resource);
free(surface->title);
free(surface->app_id);
free(surface->pixels);
free(surface);
}
/* ===================== wl_compositor ===================== */
static void compositor_create_surface(struct wl_client *client,
struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *surf = wl_resource_create(client,
&wl_surface_interface, wl_resource_get_version(resource), id);
if (!surf) { wl_client_post_no_memory(client); return; }
struct tabbed_surface *surface = calloc(1, sizeof(*surface));
if (!surface) { wl_resource_destroy(surf); return; }
surface->resource = surf;
surface->client = client;
wl_resource_set_user_data(surf, surface);
wl_resource_set_implementation(surf, &surface_impl, surface, NULL);
surface->surface_destroy_listener.notify = surface_destroyed_cb;
wl_resource_add_destroy_listener(surf, &surface->surface_destroy_listener);
wl_list_insert(&compositor.surfaces, &surface->link);
}
static void compositor_create_region(struct wl_client *client,
struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *reg = wl_resource_create(client,
&wl_region_interface, wl_resource_get_version(resource), id);
if (!reg) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(reg, &region_impl, NULL, NULL);
}
static const struct wl_compositor_interface compositor_impl = {
.create_surface = compositor_create_surface,
.create_region = compositor_create_region,
};
/* ===================== wl_surface ===================== */
static void surface_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void surface_attach(struct wl_client *client, struct wl_resource *resource,
struct wl_resource *buffer, int32_t x, int32_t y)
{
(void)client; (void)x; (void)y;
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (buffer) {
struct tabbed_buffer *b = wl_resource_get_user_data(buffer);
if (b) {
surface->buffer = b;
surface->has_buffer = true;
} else {
surface->buffer = NULL;
surface->has_buffer = false;
}
} else {
surface->buffer = NULL;
surface->has_buffer = false;
}
}
static void surface_damage(struct wl_client *client, struct wl_resource *resource,
int32_t x, int32_t y, int32_t w, int32_t h)
{
(void)client; (void)resource; (void)x; (void)y; (void)w; (void)h;
}
static void surface_frame(struct wl_client *client, struct wl_resource *resource,
uint32_t callback)
{
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (surface->frame_callback) {
wl_resource_destroy(surface->frame_callback);
surface->frame_callback = NULL;
}
surface->frame_callback = wl_resource_create(client,
&wl_callback_interface, wl_resource_get_version(resource), callback);
if (!surface->frame_callback)
wl_client_post_no_memory(client);
}
static void surface_set_opaque_region(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *region)
{
(void)client; (void)resource; (void)region;
}
static void surface_set_input_region(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *region)
{
(void)client; (void)resource; (void)region;
}
static void copy_buffer_to_display(struct tabbed_surface *surface) {
struct tabbed_output *out = &compositor.output;
if (!out->data || !surface->pixels || out->width <= 0 || out->height <= 0 || out->stride <= 0)
return;
size_t out_size = (size_t)out->height * (size_t)out->stride;
memset(out->data, 0x22, out_size);
/* Blit the client's content. GTK3 apps render CSD shadow margins around
their content and report where the content starts via
xdg_surface.set_window_geometry (e.g. galculator: geom 25,25). Without
honoring that we would show the shadow as a margin on the left/top.
Clients that never set geometry leave geom_width/height == 0; fall
back to the full buffer then. foot reports geom_y = -26, so clamp
negative offsets to 0. */
int src_x = 0, src_y = 0;
int src_w = surface->pixel_width;
int src_h = surface->pixel_height;
if (surface->geom_width > 0 && surface->geom_height > 0) {
src_x = (surface->geom_x > 0) ? surface->geom_x : 0;
src_y = (surface->geom_y > 0) ? surface->geom_y : 0;
src_w = surface->geom_width;
src_h = surface->geom_height;
if (src_x >= surface->pixel_width) src_x = surface->pixel_width - 1;
if (src_y >= surface->pixel_height) src_y = surface->pixel_height - 1;
if (src_w > surface->pixel_width - src_x)
src_w = surface->pixel_width - src_x;
if (src_h > surface->pixel_height - src_y)
src_h = surface->pixel_height - src_y;
}
int cw = (src_w < out->width) ? src_w : out->width;
int ch = (src_h < out->height) ? src_h : out->height;
if (cw <= 0 || ch <= 0) return;
if (surface->pixel_stride <= 0 || out->stride <= 0) return;
if (surface->pixels == NULL || out->data == NULL) return;
for (int y = 0; y < ch; y++) {
void *dst = out->data + y * out->stride;
void *src = surface->pixels + (src_y + y) * surface->pixel_stride
+ src_x * 4;
if ((unsigned char*)dst < (unsigned char*)out->data ||
(unsigned char*)dst + cw*4 > (unsigned char*)out->data + out->height*out->stride) break;
if ((unsigned char*)src < (unsigned char*)surface->pixels ||
(unsigned char*)src + cw*4 > (unsigned char*)surface->pixels + surface->pixel_height*surface->pixel_stride) break;
memcpy(dst, src, cw * 4);
}
}
static void surface_commit(struct wl_client *client, struct wl_resource *resource) {
(void)client;
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (!surface) return;
/* xdg_shell: first commit without buffer sends configure */
if (surface->xdg_toplevel && !surface->configured) {
surface->configured = true;
surface->configure_serial = ++compositor.serial;
if (surface->xdg_surface)
xdg_surface_send_configure(surface->xdg_surface, surface->configure_serial);
struct wl_array states;
wl_array_init(&states);
uint32_t *s = wl_array_add(&states, sizeof(uint32_t));
if (s) *s = XDG_TOPLEVEL_STATE_ACTIVATED;
xdg_toplevel_send_configure(surface->xdg_toplevel,
compositor.output.width, compositor.output.height, &states);
wl_array_release(&states);
if (surface->tab_label && surface->title)
gtk_label_set_text(GTK_LABEL(surface->tab_label), surface->title);
/* Dispatch the pending frame callback. Several clients (notably
foot) request wl_surface.frame on the first commit and will not
render their real buffer until wl_callback.done is sent, so it
must fire even on this early configure path. */
if (surface->frame_callback) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint32_t ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
wl_callback_send_done(surface->frame_callback, ms);
wl_resource_destroy(surface->frame_callback);
surface->frame_callback = NULL;
}
return;
}
/* Buffer commit — cache pixels for safe access later */
if (surface->has_buffer && surface->buffer) {
struct tabbed_buffer *buf = surface->buffer;
if (buf->pool && buf->pool->data) {
const size_t bpp = 4;
/* Defensive check: never read outside the mapped pool even if a
client sent a bad attach after a valid create_buffer. */
if (buf->offset >= 0 && buf->stride > 0 &&
buf->width > 0 && buf->height > 0 &&
buf->stride >= buf->width * (int)bpp &&
(size_t)buf->offset +
(size_t)buf->stride * (size_t)buf->height <= buf->pool->size) {
surface->pixel_width = buf->width;
surface->pixel_height = buf->height;
surface->pixel_stride = buf->stride;
size_t size = (size_t)buf->stride * (size_t)buf->height;
surface->pixels = malloc(size);
if (surface->pixels)
memcpy(surface->pixels, buf->pool->data + buf->offset, size);
} else {
/* Invalid geometry: drop the frame, keep a clean surface. */
free(surface->pixels);
surface->pixels = NULL;
wl_client_post_implementation_error(surface->client,
"invalid buffer geometry on commit");
}
/* We deep-copy the above so we no longer need the client's
buffer. However we must NOT release the buffer currently
being committed: GTK3 reuses a released committed buffer as
its next staging surface, so releasing it again on a
recommit would hit its staging/destroy bookkeeping and
abort the client (cairo refcount assert on resize). Instead
we defer: on this commit, release only the buffer from the
previous commit (if any and different), then remember the
current one to release on the next commit. */
if (surface->pending_release &&
surface->pending_release != buf)
wl_buffer_send_release(surface->pending_release->resource);
surface->pending_release = buf;
}
if (surface == compositor.active_surface) {
copy_buffer_to_display(surface);
gtk_widget_queue_draw(compositor.canvas);
}
} else {
if (surface->pending_release)
wl_buffer_send_release(surface->pending_release->resource);
surface->pending_release = NULL;
free(surface->pixels);
surface->pixels = NULL;
if (surface == compositor.active_surface) {
struct tabbed_output *o = &compositor.output;
if (o->data && o->height > 0 && o->stride > 0)
memset(o->data, 0x22, (size_t)o->height * (size_t)o->stride);
gtk_widget_queue_draw(compositor.canvas);
}
}
/* frame callback */
if (surface->frame_callback) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint32_t ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
wl_callback_send_done(surface->frame_callback, ms);
wl_resource_destroy(surface->frame_callback);
surface->frame_callback = NULL;
}
}
static void surface_set_buffer_scale(struct wl_client *client,
struct wl_resource *resource,
int32_t scale)
{
(void)client; (void)resource; (void)scale;
}
static void surface_set_buffer_transform(struct wl_client *client,
struct wl_resource *resource,
int32_t transform)
{
(void)client; (void)resource; (void)transform;
}
static void surface_damage_buffer(struct wl_client *client,
struct wl_resource *resource,
int32_t x, int32_t y, int32_t w, int32_t h)
{
(void)client; (void)resource; (void)x; (void)y; (void)w; (void)h;
}
static const struct wl_surface_interface surface_impl = {
.destroy = surface_destroy,
.attach = surface_attach,
.damage = surface_damage,
.frame = surface_frame,
.set_opaque_region = surface_set_opaque_region,
.set_input_region = surface_set_input_region,
.commit = surface_commit,
.set_buffer_transform = surface_set_buffer_transform,
.set_buffer_scale = surface_set_buffer_scale,
.damage_buffer = surface_damage_buffer,
};
/* ===================== wl_region ===================== */
static void region_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void region_add(struct wl_client *client, struct wl_resource *resource,
int32_t x, int32_t y, int32_t w, int32_t h)
{
(void)client; (void)resource; (void)x; (void)y; (void)w; (void)h;
}
static void region_subtract(struct wl_client *client, struct wl_resource *resource,
int32_t x, int32_t y, int32_t w, int32_t h)
{
(void)client; (void)resource; (void)x; (void)y; (void)w; (void)h;
}
static const struct wl_region_interface region_impl = {
.destroy = region_destroy,
.add = region_add,
.subtract = region_subtract,
};
/* ===================== wl_shm ===================== */
static void pool_destroy_resource(struct wl_resource *resource) {
struct tabbed_pool *pool = wl_resource_get_user_data(resource);
if (!pool) return;
/* Clear any surface caches that reference this pool's buffers */
struct tabbed_surface *s;
wl_list_for_each(s, &compositor.surfaces, link) {
if (s->pending_release && s->pending_release->pool == pool)
s->pending_release = NULL;
if (s->buffer && s->buffer->pool == pool) {
s->buffer = NULL;
s->has_buffer = false;
free(s->pixels);
s->pixels = NULL;
}
}
munmap(pool->data, pool->size);
close(pool->fd);
free(pool);
}
static void buffer_destroy_request(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void buffer_destroy_resource(struct wl_resource *resource) {
struct tabbed_buffer *buf = wl_resource_get_user_data(resource);
struct tabbed_surface *s;
wl_list_for_each(s, &compositor.surfaces, link) {
if (s->pending_release == buf)
s->pending_release = NULL;
if (s->buffer == buf) {
s->buffer = NULL;
s->has_buffer = false;
}
}
free(buf);
}
static const struct wl_buffer_interface buffer_impl = {
.destroy = buffer_destroy_request,
};
static void pool_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void pool_resize(struct wl_client *client, struct wl_resource *resource,
int32_t size)
{
(void)client;
struct tabbed_pool *pool = wl_resource_get_user_data(resource);
if (size <= (int32_t)pool->size) return;
unsigned char *ndata = mremap(pool->data, pool->size, size, MREMAP_MAYMOVE);
if (ndata == MAP_FAILED) return;
pool->data = ndata;
pool->size = size;
}
static void pool_create_buffer(struct wl_client *client, struct wl_resource *resource,
uint32_t id, int32_t offset,
int32_t width, int32_t height,
int32_t stride, uint32_t format)
{
(void)client;
struct tabbed_pool *pool = wl_resource_get_user_data(resource);
if (format != WL_SHM_FORMAT_ARGB8888 && format != WL_SHM_FORMAT_XRGB8888) {
wl_resource_post_error(resource, WL_SHM_ERROR_INVALID_FORMAT,
"unsupported format %u", format);
return;
}
/* Validate the client-supplied layout against the pool before accepting
it. Any negative size/offset or a stride smaller than one scanline per
BPP is malformed; the buffer must also fit inside the pool. Rejecting
here prevents out-of-bounds reads at commit time (see surface_commit). */
const int bpp = 4;
if (offset < 0 || width <= 0 || height <= 0 ||
stride < width * bpp) {
wl_resource_post_error(resource, WL_SHM_ERROR_INVALID_STRIDE,
"invalid buffer geometry (%d,%d,%d,off %d)",
width, height, stride, offset);
return;
}
if ((size_t)stride * (size_t)height > SIZE_MAX ||
(size_t)offset > (size_t)pool->size ||
(size_t)offset + (size_t)stride * (size_t)height > (size_t)pool->size) {
wl_resource_post_error(resource, WL_SHM_ERROR_INVALID_STRIDE,
"buffer exceeds pool size");
return;
}
struct wl_resource *buf_res = wl_resource_create(client,
&wl_buffer_interface, wl_resource_get_version(resource), id);
if (!buf_res) { wl_client_post_no_memory(client); return; }
struct tabbed_buffer *buf = calloc(1, sizeof(*buf));
if (!buf) { wl_resource_destroy(buf_res); return; }
buf->resource = buf_res;
buf->pool = pool;
buf->offset = offset;
buf->width = width;
buf->height = height;
buf->stride = stride;
buf->format = format;
wl_resource_set_user_data(buf_res, buf);
wl_resource_set_implementation(buf_res, &buffer_impl, buf, buffer_destroy_resource);
}
static const struct wl_shm_pool_interface pool_impl = {
.destroy = pool_destroy,
.resize = pool_resize,
.create_buffer = pool_create_buffer,
};
static void shm_create_pool(struct wl_client *client, struct wl_resource *resource,
uint32_t id, int32_t fd, int32_t size)
{
(void)client;
struct tabbed_pool *pool = malloc(sizeof(*pool));
if (!pool) { close(fd); wl_client_post_no_memory(client); return; }
pool->fd = fd;
pool->size = size;
pool->data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (pool->data == MAP_FAILED) {
close(fd);
free(pool);
wl_client_post_no_memory(client);
return;
}
struct wl_resource *pool_res = wl_resource_create(client,
&wl_shm_pool_interface, wl_resource_get_version(resource), id);
if (!pool_res) {
munmap(pool->data, size); close(fd); free(pool);
wl_client_post_no_memory(client);
return;
}
wl_resource_set_implementation(pool_res, &pool_impl, pool, pool_destroy_resource);
}
static const struct wl_shm_interface shm_impl = {
.create_pool = shm_create_pool,
};
/* ===================== xdg_wm_base ===================== */
static void xdg_wm_base_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) { (void)c;(void)r;(void)w;(void)h; }
static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y, int32_t w, int32_t h) { (void)c;(void)r;(void)x;(void)y;(void)w;(void)h; }
static void positioner_set_anchor(struct wl_client *c, struct wl_resource *r, uint32_t a) { (void)c;(void)r;(void)a; }
static void positioner_set_gravity(struct wl_client *c, struct wl_resource *r, uint32_t g) { (void)c;(void)r;(void)g; }
static void positioner_set_constraint_adjustment(struct wl_client *c, struct wl_resource *r, uint32_t ca) { (void)c;(void)r;(void)ca; }
static void positioner_set_offset(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) { (void)c;(void)r;(void)x;(void)y; }
static void positioner_set_reactive(struct wl_client *c, struct wl_resource *r) { (void)c;(void)r; }
static void positioner_set_parent_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) { (void)c;(void)r;(void)w;(void)h; }
static void positioner_set_parent_configure(struct wl_client *c, struct wl_resource *r, uint32_t s) { (void)c;(void)r;(void)s; }
static const struct xdg_positioner_interface positioner_impl = {
.destroy = positioner_destroy,
.set_size = positioner_set_size,
.set_anchor_rect = positioner_set_anchor_rect,
.set_anchor = positioner_set_anchor,
.set_gravity = positioner_set_gravity,
.set_constraint_adjustment = positioner_set_constraint_adjustment,
.set_offset = positioner_set_offset,
.set_reactive = positioner_set_reactive,
.set_parent_size = positioner_set_parent_size,
.set_parent_configure = positioner_set_parent_configure,
};
static void xdg_wm_base_create_positioner(struct wl_client *client,
struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *pos = wl_resource_create(client,
&xdg_positioner_interface, wl_resource_get_version(resource), id);
if (!pos) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(pos, &positioner_impl, NULL, NULL);
}
static void xdg_wm_base_get_xdg_surface(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
struct wl_resource *surface_resource)
{
(void)client;
struct tabbed_surface *surface = wl_resource_get_user_data(surface_resource);
if (!surface) return;
if (surface->xdg_surface) {
wl_resource_post_error(surface_resource,
XDG_WM_BASE_ERROR_ROLE, "surface already has a role");
return;
}
struct wl_resource *xdg_res = wl_resource_create(client,
&xdg_surface_interface, wl_resource_get_version(resource), id);
if (!xdg_res) { wl_client_post_no_memory(client); return; }
surface->xdg_surface = xdg_res;
wl_resource_set_user_data(xdg_res, surface);
wl_resource_set_implementation(xdg_res, &xdg_surface_impl, surface, NULL);
}
static void xdg_wm_base_pong(struct wl_client *client,
struct wl_resource *resource,
uint32_t serial)
{
(void)client; (void)resource; (void)serial;
}
static const struct xdg_wm_base_interface xdg_wm_base_impl = {
.destroy = xdg_wm_base_destroy,
.create_positioner = xdg_wm_base_create_positioner,
.get_xdg_surface = xdg_wm_base_get_xdg_surface,
.pong = xdg_wm_base_pong,
};
/* ===================== xdg_surface ===================== */
static void xdg_surface_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void xdg_surface_get_toplevel(struct wl_client *client,
struct wl_resource *resource,
uint32_t id)
{
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (!surface) return;
struct wl_resource *toplevel = wl_resource_create(client,
&xdg_toplevel_interface, wl_resource_get_version(resource), id);
if (!toplevel) { wl_client_post_no_memory(client); return; }
surface->xdg_toplevel = toplevel;
wl_resource_set_user_data(toplevel, surface);
wl_resource_set_implementation(toplevel, &xdg_toplevel_impl, surface, NULL);
/* Associate with an empty app slot */
for (int i = 0; i < compositor.num_apps; i++) {
if (compositor.apps[i]->surface == NULL) {
compositor.apps[i]->surface = surface;
surface->tab_index = compositor.apps[i]->tab_index;
surface->tab_label = compositor.apps[i]->tab_label;
break;
}
}
/* Make first surface active */
if (!compositor.active_surface)
compositor.active_surface = surface;
}
static void xdg_surface_get_popup(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
struct wl_resource *parent,
struct wl_resource *positioner)
{
(void)parent; (void)positioner;
struct wl_resource *popup = wl_resource_create(client,
&xdg_popup_interface, wl_resource_get_version(resource), id);
if (!popup) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(popup, &xdg_popup_impl, NULL, NULL);
}
static void xdg_surface_set_window_geometry(struct wl_client *client,
struct wl_resource *resource,
int32_t x, int32_t y,
int32_t width, int32_t height)
{
(void)client;
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (!surface) return;
surface->geom_x = x;
surface->geom_y = y;
surface->geom_width = width;
surface->geom_height = height;
}
static void xdg_surface_ack_configure(struct wl_client *client,
struct wl_resource *resource,
uint32_t serial)
{
(void)client; (void)resource; (void)serial;
}
static const struct xdg_surface_interface xdg_surface_impl = {
.destroy = xdg_surface_destroy,
.get_toplevel = xdg_surface_get_toplevel,
.get_popup = xdg_surface_get_popup,
.set_window_geometry = xdg_surface_set_window_geometry,
.ack_configure = xdg_surface_ack_configure,
};
/* ===================== xdg_toplevel ===================== */
static void xdg_toplevel_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void xdg_toplevel_set_parent(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *parent)
{
(void)client; (void)resource; (void)parent;
}
static void xdg_toplevel_set_title(struct wl_client *client,
struct wl_resource *resource,
const char *title)
{
(void)client;
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (!surface) return;
free(surface->title);
surface->title = g_strdup(title);
if (surface->tab_label && title && title[0])
gtk_label_set_text(GTK_LABEL(surface->tab_label), title);
}
static void xdg_toplevel_set_app_id(struct wl_client *client,
struct wl_resource *resource,
const char *app_id)
{
(void)client;
struct tabbed_surface *surface = wl_resource_get_user_data(resource);
if (!surface) return;
free(surface->app_id);
surface->app_id = g_strdup(app_id);
}
static void xdg_toplevel_show_window_menu(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *seat,
uint32_t serial,
int32_t x, int32_t y)
{
(void)client; (void)resource; (void)seat; (void)serial; (void)x; (void)y;
}
static void xdg_toplevel_move(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *seat,
uint32_t serial)
{
(void)client; (void)resource; (void)seat; (void)serial;
}
static void xdg_toplevel_resize(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *seat,
uint32_t serial,
uint32_t edges)
{
(void)client; (void)resource; (void)seat; (void)serial; (void)edges;
}
static void xdg_toplevel_set_max_size(struct wl_client *client,
struct wl_resource *resource,
int32_t width, int32_t height)
{
(void)client; (void)resource; (void)width; (void)height;
}
static void xdg_toplevel_set_min_size(struct wl_client *client,
struct wl_resource *resource,
int32_t width, int32_t height)
{
(void)client; (void)resource; (void)width; (void)height;
}
static void xdg_toplevel_set_maximized(struct wl_client *client,
struct wl_resource *resource)
{
(void)client; (void)resource;
}
static void xdg_toplevel_unset_maximized(struct wl_client *client,
struct wl_resource *resource)
{
(void)client; (void)resource;
}
static void xdg_toplevel_set_fullscreen(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *output)
{
(void)client; (void)resource; (void)output;
}
static void xdg_toplevel_unset_fullscreen(struct wl_client *client,
struct wl_resource *resource)
{
(void)client; (void)resource;
}
static void xdg_toplevel_set_minimized(struct wl_client *client,
struct wl_resource *resource)
{
(void)client; (void)resource;
}
static const struct xdg_toplevel_interface xdg_toplevel_impl = {
.destroy = xdg_toplevel_destroy,
.set_parent = xdg_toplevel_set_parent,
.set_title = xdg_toplevel_set_title,
.set_app_id = xdg_toplevel_set_app_id,
.show_window_menu = xdg_toplevel_show_window_menu,
.move = xdg_toplevel_move,
.resize = xdg_toplevel_resize,
.set_max_size = xdg_toplevel_set_max_size,
.set_min_size = xdg_toplevel_set_min_size,
.set_maximized = xdg_toplevel_set_maximized,
.unset_maximized = xdg_toplevel_unset_maximized,
.set_fullscreen = xdg_toplevel_set_fullscreen,
.unset_fullscreen = xdg_toplevel_unset_fullscreen,
.set_minimized = xdg_toplevel_set_minimized,
};
/* ===================== xdg_popup ===================== */
static void xdg_popup_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static void xdg_popup_grab(struct wl_client *client, struct wl_resource *resource,
struct wl_resource *seat, uint32_t serial)
{
(void)client; (void)resource; (void)seat; (void)serial;
}
static void xdg_popup_reposition(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *positioner,
uint32_t token)
{
(void)client; (void)resource; (void)positioner; (void)token;
}
static const struct xdg_popup_interface xdg_popup_impl = {
.destroy = xdg_popup_destroy,
.grab = xdg_popup_grab,
.reposition = xdg_popup_reposition,
};
/* ===================== wl_output ===================== */
static void output_send_all(struct wl_resource *res) {
wl_output_send_geometry(res, 0, 0,
compositor.output.width * 6, compositor.output.height * 6,
WL_OUTPUT_SUBPIXEL_UNKNOWN,
"Tabbed Compositor", "Virtual Output",
WL_OUTPUT_TRANSFORM_NORMAL);
wl_output_send_mode(res,
WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED,
compositor.output.width, compositor.output.height, 60000);
if (wl_resource_get_version(res) >= WL_OUTPUT_SCALE_SINCE_VERSION)
wl_output_send_scale(res, 1);
if (wl_resource_get_version(res) >= WL_OUTPUT_DONE_SINCE_VERSION)
wl_output_send_done(res);
}
static void output_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_output_interface, version < 2 ? version : 2, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_user_data(res, &compositor.output);
wl_list_insert(&compositor.output.resources, wl_resource_get_link(res));
output_send_all(res);
}
/* ===================== wl_seat ===================== */
static void seat_destroy(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
/* ===================== Input tracking ===================== */
struct input_track {
struct wl_client *client;
struct wl_resource *kbd;
struct wl_resource *ptr;
struct wl_listener kbd_destroy;
struct wl_listener ptr_destroy;
struct wl_list link;
};
static void input_track_kbd_destroy(struct wl_listener *l, void *data) {
(void)data;
struct input_track *t = wl_container_of(l, t, kbd_destroy);
t->kbd = NULL;
}
static void input_track_ptr_destroy(struct wl_listener *l, void *data) {
(void)data;
struct input_track *t = wl_container_of(l, t, ptr_destroy);
t->ptr = NULL;
}
static struct input_track *get_input_track(struct wl_client *client) {
struct input_track *t;
wl_list_for_each(t, &compositor.input_tracks, link) {
if (t->client == client) return t;
}
t = calloc(1, sizeof(*t));
if (!t) return NULL;
t->client = client;
t->kbd_destroy.notify = input_track_kbd_destroy;
t->ptr_destroy.notify = input_track_ptr_destroy;
wl_list_insert(&compositor.input_tracks, &t->link);
return t;
}
/* ===================== wl_pointer ===================== */
static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r,
uint32_t serial, struct wl_resource *surface,
int32_t hx, int32_t hy)
{ (void)c;(void)r;(void)serial;(void)surface;(void)hx;(void)hy; }
static void pointer_release(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static const struct wl_pointer_interface pointer_impl = {
.set_cursor = pointer_set_cursor,
.release = pointer_release,
};
/* ===================== wl_keyboard ===================== */
static void keyboard_release(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static const struct wl_keyboard_interface keyboard_impl = {
.release = keyboard_release,
};
/* ===================== wl_touch ===================== */
static void touch_release(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static const struct wl_touch_interface touch_impl = {
.release = touch_release,
};
/* ===================== wl_seat ===================== */
static void seat_get_pointer(struct wl_client *client, struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *ptr = wl_resource_create(client,
&wl_pointer_interface, wl_resource_get_version(resource), id);
if (!ptr) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(ptr, &pointer_impl, NULL, NULL);
struct input_track *t = get_input_track(client);
if (t) {
t->ptr = ptr;
wl_resource_add_destroy_listener(ptr, &t->ptr_destroy);
}
}
static void seat_get_keyboard(struct wl_client *client, struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *kbd = wl_resource_create(client,
&wl_keyboard_interface, wl_resource_get_version(resource), id);
if (!kbd) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(kbd, &keyboard_impl, NULL, NULL);
struct input_track *t = get_input_track(client);
if (t) {
t->kbd = kbd;
wl_resource_add_destroy_listener(kbd, &t->kbd_destroy);
}
if (compositor.xkb_keymap_fd >= 0) {
lseek(compositor.xkb_keymap_fd, 0, SEEK_SET);
wl_keyboard_send_keymap(kbd, WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1,
compositor.xkb_keymap_fd,
compositor.xkb_keymap_size);
}
}
static void seat_get_touch(struct wl_client *client, struct wl_resource *resource,
uint32_t id)
{
struct wl_resource *touch = wl_resource_create(client,
&wl_touch_interface, wl_resource_get_version(resource), id);
if (!touch) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(touch, &touch_impl, NULL, NULL);
}
static void seat_release(struct wl_client *client, struct wl_resource *resource) {
(void)client;
wl_resource_destroy(resource);
}
static const struct wl_seat_interface seat_impl = {
.get_pointer = seat_get_pointer,
.get_keyboard = seat_get_keyboard,
.get_touch = seat_get_touch,
.release = seat_release,
};
static void seat_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_seat_interface, version < 7 ? version : 7, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &seat_impl, NULL, NULL);
wl_seat_send_capabilities(res, WL_SEAT_CAPABILITY_POINTER | WL_SEAT_CAPABILITY_KEYBOARD);
if (wl_resource_get_version(res) >= 4)
wl_seat_send_name(res, "default");
}
/* ===================== subcompositor ===================== */
static void subcompositor_destroy(struct wl_client *client, struct wl_resource *res) {
(void)client;
wl_resource_destroy(res);
}
static void subcompositor_get_subsurface(struct wl_client *client,
struct wl_resource *resource,
uint32_t id,
struct wl_resource *surface,
struct wl_resource *parent)
{
(void)resource; (void)surface; (void)parent;
struct wl_resource *sub = wl_resource_create(client,
&wl_subsurface_interface, 1, id);
if (!sub) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(sub, &subsurface_impl, NULL, NULL);
}
/* ===================== wl_subsurface ===================== */
static void subsurface_destroy(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) { (void)c;(void)r;(void)x;(void)y; }
static void subsurface_place_above(struct wl_client *c, struct wl_resource *r, struct wl_resource *sibling) { (void)c;(void)r;(void)sibling; }
static void subsurface_place_below(struct wl_client *c, struct wl_resource *r, struct wl_resource *sibling) { (void)c;(void)r;(void)sibling; }
static void subsurface_set_sync(struct wl_client *c, struct wl_resource *r) { (void)c;(void)r; }
static void subsurface_set_desync(struct wl_client *c, struct wl_resource *r) { (void)c;(void)r; }
static const struct wl_subsurface_interface subsurface_impl = {
.destroy = subsurface_destroy,
.set_position = subsurface_set_position,
.place_above = subsurface_place_above,
.place_below = subsurface_place_below,
.set_sync = subsurface_set_sync,
.set_desync = subsurface_set_desync,
};
static const struct wl_subcompositor_interface subcompositor_impl = {
.destroy = subcompositor_destroy,
.get_subsurface = subcompositor_get_subsurface,
};
/* ===================== data device manager ===================== */
static void data_offer_destroy(struct wl_client *client, struct wl_resource *res) {
(void)client; wl_resource_destroy(res);
}
static void data_offer_accept(struct wl_client *c, struct wl_resource *r,
uint32_t s, const char *t) { (void)c;(void)r;(void)s;(void)t; }
static void data_offer_receive(struct wl_client *c, struct wl_resource *r,
const char *t, int32_t fd) { (void)c;(void)r;(void)t;close(fd); }
static void data_offer_finish(struct wl_client *c, struct wl_resource *r) { (void)c;(void)r; }
static void data_offer_set_actions(struct wl_client *c, struct wl_resource *r,
uint32_t d, uint32_t a) { (void)c;(void)r;(void)d;(void)a; }
static const struct wl_data_offer_interface data_offer_impl = {
.destroy = data_offer_destroy,
.accept = data_offer_accept,
.receive = data_offer_receive,
.finish = data_offer_finish,
.set_actions = data_offer_set_actions,
};
static void data_source_destroy(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static void data_source_offer(struct wl_client *c, struct wl_resource *r, const char *t) { (void)c;(void)r;(void)t; }
static void data_source_set_actions(struct wl_client *c, struct wl_resource *r, uint32_t a) { (void)c;(void)r;(void)a; }
static const struct wl_data_source_interface data_source_impl = {
.destroy = data_source_destroy,
.offer = data_source_offer,
.set_actions = data_source_set_actions,
};
static void data_device_start_drag(struct wl_client *c, struct wl_resource *r,
struct wl_resource *s, struct wl_resource *o,
struct wl_resource *i, uint32_t serial)
{ (void)c;(void)r;(void)s;(void)o;(void)i;(void)serial; }
static void data_device_set_selection(struct wl_client *c, struct wl_resource *r,
struct wl_resource *s, uint32_t serial)
{ (void)c;(void)r;(void)s;(void)serial; }
static void data_device_release(struct wl_client *c, struct wl_resource *r) { (void)c; wl_resource_destroy(r); }
static const struct wl_data_device_interface data_device_impl = {
.start_drag = data_device_start_drag,
.set_selection = data_device_set_selection,
.release = data_device_release,
};
static void data_device_manager_create_data_source(struct wl_client *c,
struct wl_resource *r,
uint32_t id)
{
struct wl_resource *src = wl_resource_create(c,
&wl_data_source_interface, wl_resource_get_version(r), id);
if (!src) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(src, &data_source_impl, NULL, NULL);
}
static void data_device_manager_get_data_device(struct wl_client *c,
struct wl_resource *r,
uint32_t id,
struct wl_resource *seat)
{
(void)seat;
struct wl_resource *dev = wl_resource_create(c,
&wl_data_device_interface, wl_resource_get_version(r), id);
if (!dev) { wl_client_post_no_memory(c); return; }
wl_resource_set_implementation(dev, &data_device_impl, NULL, NULL);
}
static const struct wl_data_device_manager_interface data_device_manager_impl = {
.create_data_source = data_device_manager_create_data_source,
.get_data_device = data_device_manager_get_data_device,
};
/* ===================== Global bind functions ===================== */
static void bind_compositor(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_compositor_interface, version < 5 ? version : 5, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &compositor_impl, NULL, NULL);
}
static void bind_shm(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_shm_interface, version < 2 ? version : 2, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &shm_impl, NULL, NULL);
wl_shm_send_format(res, WL_SHM_FORMAT_ARGB8888);
wl_shm_send_format(res, WL_SHM_FORMAT_XRGB8888);
}
static void bind_xdg_wm_base(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&xdg_wm_base_interface, version < 6 ? version : 6, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &xdg_wm_base_impl, NULL, NULL);
}
static void bind_subcompositor(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_subcompositor_interface, version, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &subcompositor_impl, NULL, NULL);
}
static void bind_data_device_manager(struct wl_client *client, void *data,
uint32_t version, uint32_t id)
{
(void)data;
struct wl_resource *res = wl_resource_create(client,
&wl_data_device_manager_interface, version < 3 ? version : 3, id);
if (!res) { wl_client_post_no_memory(client); return; }
wl_resource_set_implementation(res, &data_device_manager_impl, NULL, NULL);
}
static gboolean flush_resize_cb(gpointer data);
/* ===================== Output resize ===================== */
static void output_resize(int width, int height) {
if (width < 50) width = 50;
if (height < 50) height = 50;
struct tabbed_output *out = &compositor.output;
out->width = width;
out->height = height;
out->stride = width * 4;
free(out->data);
out->data = calloc(1, height * out->stride);
if (out->data)
memset(out->data, 0x22, height * out->stride);
/* Notify all outputs */
struct wl_resource *res;
wl_list_for_each(res, &out->resources, link)
output_send_all(res);
/* Reconfigure ALL surfaces with new size */
struct tabbed_surface *surf;
struct wl_array states;
wl_array_init(&states);
uint32_t *s = wl_array_add(&states, sizeof(uint32_t));
if (s) *s = XDG_TOPLEVEL_STATE_ACTIVATED;
wl_list_for_each(surf, &compositor.surfaces, link) {
if (surf->xdg_toplevel) {
compositor.serial++;
xdg_toplevel_send_configure(surf->xdg_toplevel,
width, height, &states);
if (surf->xdg_surface) {
xdg_surface_send_configure(surf->xdg_surface, compositor.serial);
surf->configure_serial = compositor.serial;
}
}
}
wl_array_release(&states);
wl_display_flush_clients(compositor.display);
}
/* ===================== Set active surface ===================== */
static struct wl_resource *kbd_for_client(struct wl_client *client) {
struct input_track *t;
wl_list_for_each(t, &compositor.input_tracks, link)
if (t->client == client) return t->kbd;
return NULL;
}
static struct wl_resource *ptr_for_client(struct wl_client *client) {
struct input_track *t;
wl_list_for_each(t, &compositor.input_tracks, link)
if (t->client == client) return t->ptr;
return NULL;
}
static void send_kbd_enter(struct tabbed_surface *surface) {
if (!surface) return;
struct wl_resource *kbd = kbd_for_client(surface->client);
if (!kbd) return;
struct wl_array keys;
wl_array_init(&keys);
compositor.input_serial++;
wl_keyboard_send_enter(kbd, compositor.input_serial,
surface->resource, &keys);
wl_array_release(&keys);
compositor.kbd_focus = surface;
compositor.kbd_resource = kbd;
wl_display_flush_clients(compositor.display);
}
static void send_kbd_leave(struct tabbed_surface *surface) {
if (!surface || !compositor.kbd_focus) return;
if (compositor.kbd_focus != surface) return;
struct wl_resource *kbd = kbd_for_client(surface->client);
if (!kbd) return;
compositor.input_serial++;
wl_keyboard_send_leave(kbd, compositor.input_serial,
surface->resource);
compositor.kbd_focus = NULL;
wl_display_flush_clients(compositor.display);
}
static void set_active_surface(struct tabbed_surface *surface) {
if (compositor.active_surface == surface) return;
send_kbd_leave(compositor.kbd_focus);
compositor.active_surface = surface;
send_kbd_enter(surface);
if (surface && surface->pixels) {
copy_buffer_to_display(surface);
} else {
struct tabbed_output *o = &compositor.output;
if (o->data && o->height > 0 && o->stride > 0)
memset(o->data, 0x22, (size_t)o->height * (size_t)o->stride);
}
gtk_widget_queue_draw(compositor.canvas);
wl_display_flush_clients(compositor.display);
}
/* ===================== GTK canvas draw ===================== */
static gboolean canvas_draw_cb(GtkWidget *widget, cairo_t *cr, gpointer user_data) {
(void)widget; (void)user_data;
struct tabbed_output *out = &compositor.output;
if (!out->data || out->width <= 0 || out->height <= 0) {
cairo_set_source_rgb(cr, 0x22 / 255.0, 0x22 / 255.0, 0x22 / 255.0);
cairo_paint(cr);
return FALSE;
}
cairo_surface_t *image = cairo_image_surface_create_for_data(
out->data, CAIRO_FORMAT_ARGB32, out->width, out->height, out->stride);
cairo_set_source_surface(cr, image, 0, 0);
cairo_paint(cr);
cairo_surface_destroy(image);
return FALSE;
}
static gboolean canvas_resize_cb(GtkWidget *w, GtkAllocation *alloc, gpointer data) {
(void)w; (void)data;
if (alloc->width < 50 || alloc->height < 50) return FALSE;
compositor.resize_pending = true;
compositor.resize_w = alloc->width;
compositor.resize_h = alloc->height;
g_idle_add(flush_resize_cb, NULL);
return FALSE;
}
/* ===================== Input forwarding ===================== */
static gboolean canvas_key_cb(GtkWidget *w, GdkEventKey *ev, gpointer data) {
(void)w; (void)data;
if (!compositor.active_surface || !compositor.active_surface->client) return FALSE;
struct wl_resource *kbd = kbd_for_client(compositor.active_surface->client);
if (!kbd) {
fprintf(stderr, "DEBUG: no kbd for active client\n");
return FALSE;
}
if (compositor.kbd_focus != compositor.active_surface)
send_kbd_enter(compositor.active_surface);
uint32_t keycode = ev->hardware_keycode;
if (keycode >= 8) keycode -= 8;
compositor.input_serial++;
uint32_t time = ev->time;
uint32_t wl_state = (ev->type == GDK_KEY_PRESS) ?
WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED;
fprintf(stderr, "DEBUG: key %s hw=%u evdev=%u\n",
wl_state ? "press" : "release",
(unsigned)ev->hardware_keycode, (unsigned)keycode);
/* Track modifiers manually — xkb indices: 0=Shift,1=Lock,2=Ctrl,3=Alt */
static uint32_t mod_depressed = 0;
static uint32_t mod_locked = 0;
if (keycode == 42 || keycode == 54) /* shift */
mod_depressed = (mod_depressed & ~(1<<0)) | (wl_state << 0);
if (keycode == 29 || keycode == 97) /* ctrl */
mod_depressed = (mod_depressed & ~(1<<2)) | (wl_state << 2);
if (keycode == 56 || keycode == 100) /* alt */
mod_depressed = (mod_depressed & ~(1<<3)) | (wl_state << 3);
if (keycode == 58) { /* caps lock — toggles on release, index 1 */
if (!wl_state) mod_locked ^= (1 << 1);
}
wl_keyboard_send_modifiers(kbd, compositor.input_serial,
mod_depressed, 0, mod_locked, 0);
wl_keyboard_send_key(kbd, compositor.input_serial,
time, keycode, wl_state);
wl_display_flush_clients(compositor.display);
return TRUE;
}
static gboolean canvas_button_cb(GtkWidget *w, GdkEventButton *ev, gpointer data) {
(void)w; (void)data;
gtk_widget_grab_focus(w);
if (!compositor.active_surface || !compositor.active_surface->client) return FALSE;
struct wl_resource *ptr = ptr_for_client(compositor.active_surface->client);
if (!ptr) {
fprintf(stderr, "DEBUG: no ptr for active client\n");
return FALSE;
}
compositor.input_serial++;
wl_pointer_send_enter(ptr, compositor.input_serial,
compositor.active_surface->resource,
wl_fixed_from_double(ev->x),
wl_fixed_from_double(ev->y));
uint32_t button = 0;
if (ev->button == 1) button = 0x110;
else if (ev->button == 2) button = 0x111;
else if (ev->button == 3) button = 0x112;
if (button) {
compositor.input_serial++;
wl_pointer_send_button(ptr, compositor.input_serial, ev->time,
button, ev->type == GDK_BUTTON_PRESS ? 1 : 0);
}
int ver = wl_resource_get_version(ptr);
if (ver >= WL_POINTER_FRAME_SINCE_VERSION)
wl_pointer_send_frame(ptr);
wl_display_flush_clients(compositor.display);
fprintf(stderr, "DEBUG: button %u %s at (%.0f,%.0f)\n",
(unsigned)ev->button,
ev->type == GDK_BUTTON_PRESS ? "press" : "release",
ev->x, ev->y);
return TRUE;
}
static gboolean canvas_motion_cb(GtkWidget *w, GdkEventMotion *ev, gpointer data) {
(void)w; (void)data;
if (!compositor.active_surface || !compositor.active_surface->client) return FALSE;
struct wl_resource *ptr = ptr_for_client(compositor.active_surface->client);
if (!ptr) return FALSE;
wl_pointer_send_motion(ptr, ev->time,
wl_fixed_from_double(ev->x),
wl_fixed_from_double(ev->y));
int ver = wl_resource_get_version(ptr);
if (ver >= WL_POINTER_FRAME_SINCE_VERSION)
wl_pointer_send_frame(ptr);
wl_display_flush_clients(compositor.display);
return TRUE;
}
/* ===================== GTK tab switch ===================== */
static void tab_switch_cb(GtkNotebook *nb, GtkWidget *page, gint page_num,
gpointer user_data)
{
(void)nb; (void)page; (void)user_data;
for (int i = 0; i < compositor.num_apps; i++) {
if (compositor.apps[i]->tab_index == page_num) {
set_active_surface(compositor.apps[i]->surface);
break;
}
}
}
/* ===================== Launch client ===================== */
static void launch_client(const char *cmd, int tab_index) {
(void)tab_index;
pid_t pid = fork();
if (pid == 0) {
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
if (!xdg_runtime) xdg_runtime = "/tmp";
setenv("WAYLAND_DISPLAY", "tabbed-wayland-0", 1);
setenv("XDG_RUNTIME_DIR", xdg_runtime, 1);
setenv("GDK_BACKEND", "wayland", 1);
setenv("QT_QPA_PLATFORM", "wayland", 1);
unsetenv("DISPLAY");
execlp("/bin/sh", "sh", "-c", cmd, NULL);
_exit(127);
}
if (pid > 0)
compositor.apps[tab_index]->pid = pid;
}
/* ===================== Wayland event loop integration ===================== */
/* Idle callback to apply pending resize & launch clients */
static gboolean flush_resize_cb(gpointer data) {
(void)data;
if (!compositor.resize_pending) return FALSE;
compositor.resize_pending = false;
output_resize(compositor.resize_w, compositor.resize_h);
if (!compositor.clients_launched) {
compositor.clients_launched = true;
for (int i = 0; i < compositor.num_apps; i++)
launch_client(compositor.apps[i]->cmd, i);
}
return FALSE;
}
static gboolean wayland_io_cb(GIOChannel *channel, GIOCondition condition,
gpointer data)
{
(void)channel; (void)condition;
struct wl_display *display = data;
wl_event_loop_dispatch(wl_display_get_event_loop(display), 0);
wl_display_flush_clients(display);
if (compositor.resize_pending)
flush_resize_cb(NULL);
return TRUE;
}
/* ===================== Crash handler ===================== */
static void crash_handler(int sig, siginfo_t *info, void *ctx) {
(void)sig; (void)info; (void)ctx;
void *bt[64];
int n = backtrace(bt, 64);
write(2, "\n=== BACKTRACE ===\n", 19);
backtrace_symbols_fd(bt, n, 2);
_exit(139);
}
/* ===================== SIGCHLD handler ===================== */
static void sigchld_handler(int sig) {
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0);
}
/* ===================== Main ===================== */
int main(int argc, char *argv[]) {
memset(&compositor, 0, sizeof(compositor));
struct sigaction crash_sa = {
.sa_sigaction = crash_handler,
.sa_flags = SA_SIGINFO | SA_NODEFER,
};
sigaction(SIGSEGV, &crash_sa, NULL);
sigaction(SIGABRT, &crash_sa, NULL);
signal(SIGCHLD, sigchld_handler);
/* Parse CLI: apps after -- */
int num_requested = 0;
const char *requested_apps[MAX_APPS] = {0};
bool found_ddash = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--") == 0) { found_ddash = true; continue; }
if (found_ddash && num_requested < MAX_APPS)
requested_apps[num_requested++] = argv[i];
}
/* Initialize Wayland display */
compositor.display = wl_display_create();
if (!compositor.display) {
fprintf(stderr, "Failed to create Wayland display\n");
return 1;
}
compositor.loop = wl_display_get_event_loop(compositor.display);
compositor.xkb_keymap_fd = -1;
/* Generate xkb keymap */
compositor.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (compositor.xkb_ctx) {
struct xkb_rule_names names = {
.rules = NULL, .model = "pc105",
.layout = "us", .variant = NULL, .options = NULL,
};
compositor.xkb_keymap = xkb_keymap_new_from_names(
compositor.xkb_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (compositor.xkb_keymap) {
compositor.xkb_state = xkb_state_new(compositor.xkb_keymap);
compositor.xkb_keymap_str = xkb_keymap_get_as_string(
compositor.xkb_keymap, XKB_KEYMAP_FORMAT_TEXT_V1);
if (compositor.xkb_keymap_str) {
compositor.xkb_keymap_size = strlen(compositor.xkb_keymap_str);
compositor.xkb_keymap_fd = memfd_create("xkb-keymap", MFD_CLOEXEC);
if (compositor.xkb_keymap_fd >= 0) {
write(compositor.xkb_keymap_fd, compositor.xkb_keymap_str,
compositor.xkb_keymap_size);
lseek(compositor.xkb_keymap_fd, 0, SEEK_SET);
}
}
}
}
/* Set up output — minimal size, grows on first size-allocate */
compositor.output.width = 320;
compositor.output.height = 240;
compositor.output.stride = 320 * 4;
compositor.output.data = calloc(1, 320 * 240 * 4);
if (compositor.output.data)
memset(compositor.output.data, 0x22, 320 * 240 * 4);
wl_list_init(&compositor.output.resources);
compositor.output.global = wl_global_create(compositor.display,
&wl_output_interface, 2, &compositor.output, output_bind);
/* Register globals */
wl_global_create(compositor.display, &wl_compositor_interface, 5,
NULL, bind_compositor);
wl_global_create(compositor.display, &wl_shm_interface, 2,
NULL, bind_shm);
wl_global_create(compositor.display, &xdg_wm_base_interface, 6,
NULL, bind_xdg_wm_base);
wl_global_create(compositor.display, &wl_subcompositor_interface, 1,
NULL, bind_subcompositor);
wl_global_create(compositor.display, &wl_data_device_manager_interface, 3,
NULL, bind_data_device_manager);
wl_global_create(compositor.display, &wl_seat_interface, 7,
NULL, seat_bind);
wl_list_init(&compositor.surfaces);
wl_list_init(&compositor.input_tracks);
/* Add Wayland socket */
if (wl_display_add_socket(compositor.display, "tabbed-wayland-0") < 0) {
fprintf(stderr, "Failed to add socket 'tabbed-wayland-0': %s\n",
strerror(errno));
return 1;
}
printf("Wayland socket: tabbed-wayland-0\n");
printf("Set WAYLAND_DISPLAY=tabbed-wayland-0 to connect\n");
/* Read apps from tab-commands file, CLI --args, or defaults */
char *file_apps[MAX_APPS];
int total_apps = 0;
const char **apps_to_launch = NULL;
FILE *fp = fopen("tab-commands", "r");
if (fp) {
char line[512];
while (fgets(line, sizeof(line), fp) && total_apps < MAX_APPS) {
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
char *cr = strchr(line, '\r');
if (cr) *cr = '\0';
if (line[0] == '#' || line[0] == '\0') continue;
file_apps[total_apps] = strdup(line);
total_apps++;
}
fclose(fp);
apps_to_launch = (const char **)file_apps;
}
if (num_requested > 0) {
apps_to_launch = requested_apps;
total_apps = num_requested;
}
if (!apps_to_launch) {
apps_to_launch = default_apps;
total_apps = 0;
while (apps_to_launch[total_apps]) total_apps++;
}
if (total_apps > MAX_APPS) total_apps = MAX_APPS;
/* Force X11 backend — compositor must not use Wayland for its own UI */
gdk_set_allowed_backends("x11");
gtk_init(&argc, &argv);
/* Build UI */
compositor.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(compositor.window), "Tabbed Wayland");
gtk_window_set_default_size(GTK_WINDOW(compositor.window), 900, 700);
g_signal_connect(compositor.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(compositor.window), vbox);
compositor.notebook = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(compositor.notebook), TRUE);
gtk_notebook_set_show_border(GTK_NOTEBOOK(compositor.notebook), FALSE);
gtk_box_pack_start(GTK_BOX(vbox), compositor.notebook, FALSE, FALSE, 0);
compositor.canvas = gtk_drawing_area_new();
gtk_widget_set_hexpand(compositor.canvas, TRUE);
gtk_widget_set_vexpand(compositor.canvas, TRUE);
gtk_widget_set_can_focus(compositor.canvas, TRUE);
gtk_widget_add_events(compositor.canvas,
GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
GDK_POINTER_MOTION_MASK);
g_signal_connect(compositor.canvas, "draw",
G_CALLBACK(canvas_draw_cb), NULL);
g_signal_connect(compositor.canvas, "size-allocate",
G_CALLBACK(canvas_resize_cb), NULL);
g_signal_connect(compositor.canvas, "key-press-event",
G_CALLBACK(canvas_key_cb), NULL);
g_signal_connect(compositor.canvas, "key-release-event",
G_CALLBACK(canvas_key_cb), NULL);
g_signal_connect(compositor.canvas, "button-press-event",
G_CALLBACK(canvas_button_cb), NULL);
g_signal_connect(compositor.canvas, "button-release-event",
G_CALLBACK(canvas_button_cb), NULL);
g_signal_connect(compositor.canvas, "motion-notify-event",
G_CALLBACK(canvas_motion_cb), NULL);
gtk_box_pack_start(GTK_BOX(vbox), compositor.canvas, TRUE, TRUE, 0);
/* Apply dark theme */
GtkCssProvider *css = gtk_css_provider_new();
gtk_css_provider_load_from_data(css,
"window { background-color: #222; color: #ccc; }"
"notebook header { background-color: #333; }"
"notebook tab { background-color: #444; color: #ddd; padding: 4px 12px; }"
"notebook tab:checked { background-color: #555; }"
"label { color: #ccc; }", -1, NULL);
gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
GTK_STYLE_PROVIDER(css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
/* Create app entries and tabs (defer launch to after first resize) */
compositor.num_apps = total_apps;
for (int i = 0; i < total_apps; i++) {
struct tabbed_app *app = calloc(1, sizeof(*app));
app->cmd = g_strdup(apps_to_launch[i]);
app->pid = -1;
app->tab_index = i;
GtkWidget *label = gtk_label_new(apps_to_launch[i]);
app->tab_label = label;
GtkWidget *placeholder = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(compositor.notebook),
placeholder, label);
compositor.apps[i] = app;
}
g_signal_connect(compositor.notebook, "switch-page",
G_CALLBACK(tab_switch_cb), NULL);
/* Register Wayland watch — clients can connect, but won't be processed
until gtk_main runs (no blocking, no corruption) */
int wayland_fd = wl_event_loop_get_fd(compositor.loop);
GIOChannel *channel = g_io_channel_unix_new(wayland_fd);
g_io_add_watch(channel, G_IO_IN, wayland_io_cb, compositor.display);
g_io_channel_unref(channel);
gtk_widget_show_all(compositor.window);
gtk_widget_grab_focus(compositor.canvas);
/* Clients are launched by canvas_resize_cb on first size-allocate */
gtk_main();
/* Cleanup */
for (int i = 0; i < compositor.num_apps; i++) {
if (compositor.apps[i]->pid > 0)
kill(compositor.apps[i]->pid, SIGTERM);
free(compositor.apps[i]->cmd);
free(compositor.apps[i]);
}
free(compositor.output.data);
if (compositor.xkb_keymap_fd >= 0) close(compositor.xkb_keymap_fd);
free(compositor.xkb_keymap_str);
if (compositor.xkb_state) xkb_state_unref(compositor.xkb_state);
if (compositor.xkb_keymap) xkb_keymap_unref(compositor.xkb_keymap);
if (compositor.xkb_ctx) xkb_context_unref(compositor.xkb_ctx);
return 0;
}