summaryrefslogtreecommitdiff
path: root/src/wayland/surface.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-11 21:53:42 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-11 21:53:42 +0300
commit927ae621c9a3d46dbc401b8e9ea7bbed1bf2e97b (patch)
treeaeb00c16770dcf5a36d28c1167cc39c6759059fb /src/wayland/surface.c
initial: Yet Another Surface Library (YASL) 0.1.0
A minimal, modern desktop window surface library in C. Supports Wayland on Linux and Cocoa/Metal on macOS with presentation timing, fractional scaling, IME, and CSD.
Diffstat (limited to 'src/wayland/surface.c')
-rw-r--r--src/wayland/surface.c430
1 files changed, 430 insertions, 0 deletions
diff --git a/src/wayland/surface.c b/src/wayland/surface.c
new file mode 100644
index 0000000..da334e4
--- /dev/null
+++ b/src/wayland/surface.c
@@ -0,0 +1,430 @@
+#include "internal.h"
+#include <assert.h>
+
+static void display_free(struct yasl_display *c);
+
+static void display_cancel_read(struct yasl_display *c)
+{
+ if (!c->reading)
+ return;
+ wl_display_cancel_read(c->wl);
+ c->reading = false;
+}
+
+static void report_display_error(struct yasl_display *c)
+{
+ int err = wl_display_get_error(c->wl);
+ if (!err)
+ return;
+ uint32_t object_id = 0;
+ const struct wl_interface *iface = NULL;
+ uint32_t protocol_err = wl_display_get_protocol_error(c->wl, &iface, &object_id);
+ if (protocol_err)
+ fprintf(stderr, "[yasl] protocol error %u on %s (object %u)\n",
+ protocol_err, iface ? iface->name : "?", object_id);
+ else
+ fprintf(stderr, "[yasl] display error %d\n", err);
+}
+
+void display_enter(struct yasl_display *c)
+{
+ c->depth++;
+}
+
+void display_leave(struct yasl_display *c)
+{
+ if (--c->depth > 0)
+ return;
+ keyboard_settle_focus(c);
+ while (c->dead) {
+ struct yasl_surface *w = c->dead;
+ c->dead = w->dead_next;
+ free(w);
+ }
+}
+
+static void display_lose(struct yasl_display *c)
+{
+ if (c->lost)
+ return;
+ c->lost = true;
+ report_display_error(c);
+ display_cancel_read(c);
+ display_enter(c);
+ for (struct yasl_surface *w = c->surfaces, *next; w; w = next) {
+ next = w->conn_next;
+ surface_fail(w);
+ }
+ display_leave(c);
+ if (c->cbs && c->cbs->lost)
+ c->cbs->lost(c->ud);
+}
+
+void surface_fail(struct yasl_surface *w)
+{
+ if (w->closed)
+ return;
+ w->closed = true;
+ if (w->cbs->close)
+ w->cbs->close(w, w->ud);
+}
+
+static int display_ready(int fd, short revents, void *ud)
+{
+ (void)fd;
+ struct yasl_display *c = ud;
+ if (c->lost)
+ return 0;
+ display_enter(c);
+ if (c->reading) {
+ c->reading = false;
+ if (!(revents & POLLIN))
+ wl_display_cancel_read(c->wl);
+ else if (wl_display_read_events(c->wl) < 0)
+ revents |= POLLERR;
+ }
+ if (wl_display_dispatch_pending(c->wl) < 0 ||
+ (revents & (POLLERR | POLLHUP | POLLNVAL)))
+ display_lose(c);
+ display_leave(c);
+ return 0;
+}
+
+void yasl_display_dispatch(struct yasl_display *c)
+{
+ if (!c || c->lost || c->reading)
+ return;
+ display_enter(c);
+ if (wl_display_dispatch_pending(c->wl) < 0)
+ display_lose(c);
+ display_leave(c);
+}
+
+static void display_arm(struct yasl_display *c)
+{
+ while (!c->reading && wl_display_prepare_read(c->wl) != 0) {
+ if (wl_display_dispatch_pending(c->wl) < 0) {
+ display_lose(c);
+ return;
+ }
+ }
+ c->reading = true;
+ short events = POLLIN;
+ if (wl_display_flush(c->wl) < 0) {
+ if (errno != EAGAIN) {
+ display_lose(c);
+ return;
+ }
+ events |= POLLOUT;
+ }
+ if (c->loop)
+ yasl_loop_set_fd_events(c->loop, c->watch, events);
+}
+
+void yasl_display_flush(struct yasl_display *c)
+{
+ if (!c || c->lost)
+ return;
+ display_enter(c);
+ for (struct yasl_surface *w = c->surfaces, *next; w; w = next) {
+ next = w->conn_next;
+ surface_draw_frame(w);
+ }
+ if (!c->lost)
+ display_arm(c);
+ display_leave(c);
+}
+
+static void display_free(struct yasl_display *c)
+{
+ conn_drag_end(c);
+ if (c->loop && c->watch > 0) yasl_loop_del_fd(c->loop, c->watch);
+ if (c->wl) display_cancel_read(c);
+ while (c->seats)
+ seat_destroy(c->seats);
+ if (c->toplevel_drag_mgr) xdg_toplevel_drag_manager_v1_destroy(c->toplevel_drag_mgr);
+ if (c->primary_selection_mgr) zwp_primary_selection_device_manager_v1_destroy(c->primary_selection_mgr);
+ if (c->data_manager) wl_data_device_manager_destroy(c->data_manager);
+ if (c->cursor_shape_mgr) wp_cursor_shape_manager_v1_destroy(c->cursor_shape_mgr);
+ if (c->cursor_theme) wl_cursor_theme_destroy(c->cursor_theme);
+ if (c->tablet_mgr) zwp_tablet_manager_v2_destroy(c->tablet_mgr);
+ if (c->text_input_mgr) zwp_text_input_manager_v3_destroy(c->text_input_mgr);
+ if (c->tearing_mgr) wp_tearing_control_manager_v1_destroy(c->tearing_mgr);
+ if (c->fifo_mgr) wp_fifo_manager_v1_destroy(c->fifo_mgr);
+ if (c->commit_timing_mgr) wp_commit_timing_manager_v1_destroy(c->commit_timing_mgr);
+ if (c->presentation) wp_presentation_destroy(c->presentation);
+ if (c->xkb_ctx) xkb_context_unref(c->xkb_ctx);
+ if (c->scale_mgr) wp_fractional_scale_manager_v1_destroy(c->scale_mgr);
+ if (c->viewporter) wp_viewporter_destroy(c->viewporter);
+ if (c->decor_mgr) zxdg_decoration_manager_v1_destroy(c->decor_mgr);
+ conn_destroy_outputs(c);
+ if (c->wm_base) xdg_wm_base_destroy(c->wm_base);
+ if (c->shm) wl_shm_destroy(c->shm);
+ if (c->compositor) wl_compositor_destroy(c->compositor);
+ if (c->registry) wl_registry_destroy(c->registry);
+ if (c->wl) wl_display_disconnect(c->wl);
+ free(c->app_id);
+ free(c);
+}
+
+struct yasl_display *yasl_display_open(struct yasl_loop *loop, const struct yasl_display_spec *spec)
+{
+ struct yasl_display *c = calloc(1, sizeof(*c));
+ if (!c) return NULL;
+ c->loop = loop;
+ if (spec) {
+ c->cbs = spec->cbs;
+ c->ud = spec->ud;
+ if (spec->app_id) c->app_id = strdup(spec->app_id);
+ }
+ c->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ c->wl = c->xkb_ctx ? wl_display_connect(NULL) : NULL;
+ if (!c->wl) goto err;
+
+ c->registry = wl_display_get_registry(c->wl);
+ if (!c->registry) goto err;
+ wl_registry_add_listener(c->registry, &registry_listener, c);
+ if (wl_display_roundtrip(c->wl) < 0 || !c->compositor || !c->shm || !c->wm_base)
+ goto err;
+
+ surface_init_cursor(c);
+ if (c->loop) {
+ c->watch = yasl_loop_add_fd(loop, wl_display_get_fd(c->wl), POLLIN, display_ready, c);
+ if (c->watch <= 0) { c->watch = 0; goto err; }
+ }
+ if (display_input_init(c) < 0)
+ goto err;
+ return c;
+
+err:
+ display_free(c);
+ return NULL;
+}
+
+void yasl_display_close(struct yasl_display *c)
+{
+ if (!c) return;
+ assert(!c->surfaces && "destroy every surface before its display");
+ display_free(c);
+}
+
+uint32_t yasl_display_caps(const struct yasl_display *c)
+{
+ if (!c) return 0;
+ uint32_t caps = YASL_CAP_POPUPS | YASL_CAP_CSD;
+ struct seat *s = conn_active_seat(c);
+ const struct wl_data_device *device = s ? clipboard_data_device(s->clipboard) : NULL;
+ if (device && wl_data_device_get_version((struct wl_data_device *)device) >= 3)
+ caps |= YASL_CAP_DRAG;
+ if (c->toplevel_drag_mgr)
+ caps |= YASL_CAP_DRAG_CARRY;
+ if (c->primary_selection_mgr)
+ caps |= YASL_CAP_PRIMARY_SELECTION;
+ return caps;
+}
+
+static void conn_link(struct yasl_display *c, struct yasl_surface *w)
+{
+ w->conn = c;
+ w->conn_next = c->surfaces;
+ c->surfaces = w;
+}
+
+static void conn_unlink(struct yasl_surface *w)
+{
+ struct yasl_display *c = w->conn;
+ if (!c) return;
+ for (struct yasl_surface **p = &c->surfaces; *p; p = &(*p)->conn_next) {
+ if (*p == w) {
+ *p = w->conn_next;
+ break;
+ }
+ }
+ if (c->paste_target == w)
+ c->paste_target = NULL;
+ for (struct seat *s = c->seats; s; s = s->next)
+ seat_forget_surface(s, w);
+ w->conn = NULL;
+}
+
+struct yasl_surface *conn_surface_for(struct yasl_display *c, struct wl_surface *s)
+{
+ for (struct yasl_surface *w = c->surfaces; w && s; w = w->conn_next)
+ if (w->surface == s)
+ return w;
+ return NULL;
+}
+
+static bool spec_valid(const struct yasl_surface_spec *spec)
+{
+ size_t bytes;
+ return spec && spec->size.x > 0 && spec->size.y > 0 &&
+ buffer_size(spec->size.x, spec->size.y, &bytes);
+}
+
+struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *c,
+ const struct yasl_surface_spec *spec)
+{
+ if (!c || c->lost || !spec_valid(spec))
+ return NULL;
+ int width = spec->size.x, height = spec->size.y;
+ display_cancel_read(c);
+ struct yasl_surface *w = calloc(1, sizeof(*w));
+ if (!w) return NULL;
+ display_enter(c);
+ conn_link(c, w);
+ yasl_surface_set_callbacks(w, NULL, NULL);
+ w->req_width = width;
+ w->req_height = height;
+ w->logical_width = width;
+ w->logical_height = height;
+ w->scale_120 = YASL_SCALE_UNIT;
+ w->pending_scale_120 = YASL_SCALE_UNIT;
+ w->scale_factor = 1.0;
+ w->size_pending = true;
+ w->clock.refresh_ns = c->outputs && c->outputs->refresh_ns
+ ? c->outputs->refresh_ns : YASL_REFRESH_DEFAULT_NS;
+
+ w->surface = wl_compositor_create_surface(c->compositor);
+ if (!w->surface) goto err;
+ if (surface_init_scaling(w) < 0) goto err;
+ if (surface_init_shell(w, spec->title) < 0) goto err;
+ w->opaque = spec->fullscreen;
+ if (spec->fullscreen)
+ xdg_toplevel_set_fullscreen(w->toplevel, NULL);
+ if (spec->tearing && c->tearing_mgr) {
+ w->tearing_control = wp_tearing_control_manager_v1_get_tearing_control(
+ c->tearing_mgr, w->surface);
+ if (w->tearing_control)
+ wp_tearing_control_v1_set_presentation_hint(w->tearing_control,
+ WP_TEARING_CONTROL_V1_PRESENTATION_HINT_ASYNC);
+ }
+
+ wl_surface_commit(w->surface);
+ if (wl_display_roundtrip(c->wl) < 0) {
+ display_lose(c);
+ goto err;
+ }
+ if (apply_pending_geometry(w) < 0) goto err;
+ if (w->closed) goto err;
+ display_leave(c);
+ return w;
+
+err:
+ yasl_surface_destroy(w);
+ display_leave(c);
+ return NULL;
+}
+
+void yasl_surface_set_callbacks(struct yasl_surface *w, const struct yasl_surface_cbs *cbs, void *ud)
+{
+ if (w && !w->closed) {
+ static const struct yasl_surface_cbs no_callbacks = {0};
+ w->cbs = cbs ? cbs : &no_callbacks;
+ w->ud = ud;
+ }
+}
+
+void yasl_surface_destroy(struct yasl_surface *w)
+{
+ if (!w || w->destroyed) return;
+ for (struct yasl_surface *o = w->conn ? w->conn->surfaces : NULL; o; o = o->conn_next)
+ assert(o->parent != w && "destroy a popup before its parent");
+ input_queue_clear(&w->input);
+ free(w->ime_preedit);
+ free(w->ime_commit);
+ free(w->ime_shown);
+ if (w->tearing_control) wp_tearing_control_v1_destroy(w->tearing_control);
+ if (w->fifo) wp_fifo_v1_destroy(w->fifo);
+ if (w->commit_timer) wp_commit_timer_v1_destroy(w->commit_timer);
+ surface_feedback_destroy(w);
+ if (w->frame_cb) wl_callback_destroy(w->frame_cb);
+ for (int i = 0; i < NBUF; i++) buffer_destroy(&w->buffers[i]);
+ if (w->frac_scale) wp_fractional_scale_v1_destroy(w->frac_scale);
+ if (w->viewport) wp_viewport_destroy(w->viewport);
+ if (w->toplevel_decor) zxdg_toplevel_decoration_v1_destroy(w->toplevel_decor);
+ if (w->toplevel) xdg_toplevel_destroy(w->toplevel);
+ if (w->popup) xdg_popup_destroy(w->popup);
+ if (w->xdg_surface) xdg_surface_destroy(w->xdg_surface);
+ if (w->surface) wl_surface_destroy(w->surface);
+ struct yasl_display *c = w->conn;
+ conn_unlink(w);
+ static const struct yasl_surface_cbs no_callbacks = {0};
+ *w = (struct yasl_surface){
+ .conn_next = w->conn_next,
+ .cbs = &no_callbacks,
+ .closed = true,
+ .destroyed = true,
+ };
+ if (c && c->depth > 0) {
+ w->dead_next = c->dead;
+ c->dead = w;
+ } else {
+ free(w);
+ }
+}
+
+struct yasl_display *yasl_surface_display(const struct yasl_surface *w)
+{
+ return w ? w->conn : NULL;
+}
+
+void yasl_surface_schedule_redraw(struct yasl_surface *w)
+{
+ if (w) w->want_draw = true;
+}
+
+struct yasl_vector2i yasl_surface_size(const struct yasl_surface *w)
+{
+ return w ? (struct yasl_vector2i){ .x = w->width, .y = w->height } : (struct yasl_vector2i){0};
+}
+
+uint32_t yasl_surface_scale_120(const struct yasl_surface *w)
+{
+ return w ? w->scale_120 : YASL_SCALE_UNIT;
+}
+
+void yasl_surface_set_background_color(struct yasl_surface *w, uint32_t bg)
+{
+ (void)w; (void)bg;
+}
+
+bool yasl_surface_start_interactive_move(struct yasl_surface *s)
+{
+ if (!s || !s->toplevel || !s->conn) return false;
+ struct seat *seat = conn_active_seat(s->conn);
+ if (!seat || !seat->proxy || !seat->press_serial) return false;
+ xdg_toplevel_move(s->toplevel, seat->proxy, seat->press_serial);
+ return true;
+}
+
+bool yasl_surface_start_interactive_resize(struct yasl_surface *s, enum yasl_resize_edge edge)
+{
+ if (!s || !s->toplevel || !s->conn || edge == YASL_EDGE_NONE) return false;
+ struct seat *seat = conn_active_seat(s->conn);
+ if (!seat || !seat->proxy || !seat->press_serial) return false;
+ xdg_toplevel_resize(s->toplevel, seat->proxy, seat->press_serial, (uint32_t)edge);
+ return true;
+}
+
+bool yasl_surface_show_window_menu(struct yasl_surface *s, struct yasl_vector2i pos)
+{
+ if (!s || !s->toplevel || !s->conn) return false;
+ struct seat *seat = conn_active_seat(s->conn);
+ if (!seat || !seat->proxy || !seat->press_serial) return false;
+ uint32_t scale = s->scale_120 ? s->scale_120 : YASL_SCALE_UNIT;
+ xdg_toplevel_show_window_menu(s->toplevel, seat->proxy, seat->press_serial,
+ yasl_scale_to_logical(pos.x, scale),
+ yasl_scale_to_logical(pos.y, scale));
+ return true;
+}
+
+void *yasl_display_get_native_handle(struct yasl_display *d)
+{
+ return d ? d->wl : NULL;
+}
+
+void *yasl_surface_get_native_handle(struct yasl_surface *s)
+{
+ return s ? s->surface : NULL;
+}