CasildaCompositor: add clipboard integration support

This commit is contained in:
Juan Pablo Ugarte 2026-07-24 18:58:19 -04:00
parent 41a4068de3
commit c432801b18
No known key found for this signature in database
7 changed files with 322 additions and 10 deletions

View file

@ -7,6 +7,24 @@ packaging changes like new dependencies or build system changes.
Casilda used even/odd minor numbers to differentiate between stable and Casilda used even/odd minor numbers to differentiate between stable and
development releases. development releases.
## Unreleased
- Add clipboard support
- Fix modifiers in pointer button press events
- Add keyboard Caps/Num/Scroll Lock support
- Fix popup of popup crash
- Fix maximized/fullscreen state handling
- Fix modifiers flags creation (Evgenii Danilin)
- Drop cursor surface listeners when the surface is destroyed (Evgenii Danilin)
- Close dup'd plane FDs when a dmabuf import fails (Evgenii Danilin)
- Advertise a valid output scale to avoid a scale-0 assert (Evgenii Danilin)
- Unref the wayland GSource (Evgenii Danilin)
- Meson config cleanup (Val Packett)
### Packaging
- Add explicit libdrm dependency
## 1.4.0 ## 1.4.0
2026-05-22 - Val Packett Release! 2026-05-22 - Val Packett Release!

View file

@ -14,6 +14,7 @@ gnome = import('gnome')
version_split = meson.project_version().split('.') version_split = meson.project_version().split('.')
gtk4_dep = dependency('gtk4', version: '>= 4.22.2') gtk4_dep = dependency('gtk4', version: '>= 4.22.2')
gio_unix_dep = dependency('gio-unix-2.0')
libdrm_dep = dependency('libdrm') libdrm_dep = dependency('libdrm')
pixman_dep = dependency('pixman-1', version: '>=0.42.0') pixman_dep = dependency('pixman-1', version: '>=0.42.0')
wayland_server_dep = dependency('wayland-server', version: '>=1.22') wayland_server_dep = dependency('wayland-server', version: '>=1.22')

255
src/casilda-clipboard.c Normal file
View file

@ -0,0 +1,255 @@
/*
* Casilda Clipboard Integration
*
* Copyright (C) 2026 Juan Pablo Ugarte
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Authors:
* Juan Pablo Ugarte <juanpablougarte@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-only
*/
#define WLR_USE_UNSTABLE 1
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <glib-unix.h>
#include <gio/gunixinputstream.h>
#include <wlr/types/wlr_data_device.h>
#include "casilda-private.h"
typedef struct
{
struct wlr_data_source base;
GdkClipboard *clipboard;
GStrv mimes;
} CasildaDataSource;
typedef struct
{
GdkClipboard *clipboard;
gchar *mime_type;
GByteArray *result;
guchar buffer[4096];
gint fd;
} ClipboardData;
static ClipboardData *
clipboard_data_new (GdkClipboard *clipboard, const gchar *mime_type, gint fd)
{
ClipboardData *data = g_new0 (ClipboardData, 1);
data->clipboard = g_object_ref (clipboard);
data->mime_type = g_strdup (mime_type);
data->result = g_byte_array_new ();
data->fd = fd;
return data;
}
static void
clipboard_data_free (ClipboardData *data)
{
g_object_unref (data->clipboard);
g_free (data->mime_type);
g_byte_array_free (data->result, TRUE);
if (data->fd)
close (data->fd);
g_free (data);
}
static void
on_clipboard_send_read_all_done (GObject *stream, GAsyncResult *result, gpointer userdata)
{
ClipboardData *data = userdata;
gsize bytes_read;
g_input_stream_read_all_finish (G_INPUT_STREAM (stream), result, &bytes_read, NULL);
if (bytes_read)
{
write (data->fd, data->buffer, bytes_read);
g_input_stream_read_all_async (G_INPUT_STREAM (stream),
data->buffer,
sizeof (data->buffer),
G_PRIORITY_DEFAULT,
NULL,
on_clipboard_send_read_all_done,
data);
}
else
{
clipboard_data_free (data);
g_object_unref (stream);
}
}
static void
on_clipboard_send_read_done (G_GNUC_UNUSED GObject *obj, GAsyncResult *result, gpointer userdata)
{
ClipboardData *data = userdata;
GInputStream *stream = gdk_clipboard_read_finish (data->clipboard, result, NULL, NULL);
if (!stream)
{
clipboard_data_free (data);
return;
}
g_input_stream_read_all_async (stream,
data->buffer,
sizeof(data->buffer),
G_PRIORITY_DEFAULT,
NULL,
on_clipboard_send_read_all_done,
data);
}
static void
casilda_data_source_send (struct wlr_data_source *data_source, const char *mime_type, int32_t fd)
{
CasildaDataSource *source = (CasildaDataSource *)data_source;
const gchar *mimes[2] = { mime_type, NULL };
if (!g_strv_contains ((const gchar * const*)source->mimes, mime_type))
return;
ClipboardData *data = clipboard_data_new (source->clipboard, mime_type, fd);
gdk_clipboard_read_async (source->clipboard,
mimes,
G_PRIORITY_DEFAULT,
NULL,
on_clipboard_send_read_done,
data);
}
static void
casilda_data_source_destroy (struct wlr_data_source *source)
{
CasildaDataSource *data_source = (CasildaDataSource *)source;
g_object_unref (data_source->clipboard);
g_strfreev (data_source->mimes);
g_free (data_source);
}
static const struct wlr_data_source_impl casilda_data_source_impl = {
.send = casilda_data_source_send,
.destroy = casilda_data_source_destroy,
};
static struct wlr_data_source *
casilda_data_source_new (GdkClipboard *clipboard)
{
CasildaDataSource *source = g_new0 (CasildaDataSource, 1);
GdkContentFormats *formats = gdk_clipboard_get_formats (clipboard);
wlr_data_source_init (&source->base, &casilda_data_source_impl);
source->clipboard = g_object_ref (clipboard);
source->mimes = g_strdupv ((GStrv) gdk_content_formats_get_mime_types (formats, NULL));
for (gchar **mime = source->mimes; mime && *mime; mime++)
{
char **dst = wl_array_add (&source->base.mime_types, sizeof(char *));
*dst = g_strdup (*mime);
}
return &source->base;
}
void
casilda_clipboard_seat_set_selection (GdkClipboard *clipboard, struct wlr_seat *seat)
{
struct wlr_data_source *source = casilda_data_source_new (clipboard);
wlr_seat_set_selection (seat, source, wl_display_next_serial (seat->display));
}
static void
on_clipboard_read_done (GObject *source, GAsyncResult *result, gpointer userdata)
{
ClipboardData *data = userdata;
gsize bytes_read;
g_input_stream_read_all_finish (G_INPUT_STREAM(source), result, &bytes_read, NULL);
if (bytes_read)
{
data->result = g_byte_array_append (data->result, data->buffer, bytes_read);
g_input_stream_read_all_async (G_INPUT_STREAM(source),
data->buffer,
sizeof(data->buffer),
G_PRIORITY_DEFAULT,
NULL,
on_clipboard_read_done,
data);
}
else
{
if (data->mime_type)
{
if (g_str_has_prefix (data->mime_type, "text/"))
{
data->result = g_byte_array_append (data->result, (const guchar *)"\0", 1);
gdk_clipboard_set_text (data->clipboard, (gchar *)data->result->data);
}
else if (g_str_has_prefix (data->mime_type, "image/"))
{
g_autoptr (GBytes) bytes = g_bytes_new_static (data->result->data, data->result->len);
g_autoptr (GdkTexture) texture = gdk_texture_new_from_bytes (bytes, NULL);
gdk_clipboard_set_texture (data->clipboard, texture);
}
}
clipboard_data_free (data);
}
}
void
casilda_clipboard_set_from_data_source (GdkClipboard *clipboard, struct wlr_data_source *source)
{
char **mime;
wl_array_for_each (mime, &source->mime_types)
{
GUnixPipe p;
if (!g_str_has_prefix (*mime, "text/") && !g_str_has_prefix (*mime, "image/"))
continue;
if (!g_unix_pipe_open (&p, O_CLOEXEC | O_NONBLOCK, NULL))
continue;
/* Write data to pipe */
wlr_data_source_send (source, *mime, g_unix_pipe_get (&p, G_UNIX_PIPE_END_WRITE));
/* Read data from pipe */
GInputStream *stream = g_unix_input_stream_new (g_unix_pipe_get (&p, G_UNIX_PIPE_END_READ), TRUE);
ClipboardData *data = clipboard_data_new (clipboard, *mime, -1);
g_input_stream_read_all_async (stream,
data->buffer,
sizeof(data->buffer),
G_PRIORITY_DEFAULT,
NULL,
on_clipboard_read_done,
data);
}
}

View file

@ -158,7 +158,7 @@
#include "casilda-config.h" #include "casilda-config.h"
#define _POSIX_C_SOURCE 199309L /* clock_gettime */ #define _POSIX_C_SOURCE 200809L /* clock_gettime */
#define WLR_USE_UNSTABLE 1 #define WLR_USE_UNSTABLE 1
#define G_LOG_DOMAIN "Casilda" #define G_LOG_DOMAIN "Casilda"
#define ENABLE_TEXTURE_DEBUG 0 #define ENABLE_TEXTURE_DEBUG 0
@ -242,6 +242,9 @@ typedef struct
/* Renderer context */ /* Renderer context */
GdkGLContext *gl_context; GdkGLContext *gl_context;
GdkClipboard *clipboard;
gulong clipboard_source;
/* Wayland display */ /* Wayland display */
struct wl_display *wl_display; struct wl_display *wl_display;
@ -2300,6 +2303,12 @@ on_casilda_compositor_frame_clock_update (G_GNUC_UNUSED GdkFrameClock *self,
wlr_output_send_frame (&priv->output); wlr_output_send_frame (&priv->output);
} }
static void
on_casilda_compositor_clipboard_changed (G_GNUC_UNUSED GObject *obj, CasildaCompositorPrivate *priv)
{
casilda_clipboard_seat_set_selection (priv->clipboard, priv->seat);
}
static void static void
casilda_compositor_realize (GtkWidget *widget) casilda_compositor_realize (GtkWidget *widget)
{ {
@ -2312,6 +2321,12 @@ casilda_compositor_realize (GtkWidget *widget)
g_signal_connect (priv->frame_clock, "update", g_signal_connect (priv->frame_clock, "update",
G_CALLBACK (on_casilda_compositor_frame_clock_update), G_CALLBACK (on_casilda_compositor_frame_clock_update),
priv); priv);
priv->clipboard = gtk_widget_get_clipboard (widget);
priv->clipboard_source =
g_signal_connect (priv->clipboard, "changed",
G_CALLBACK (on_casilda_compositor_clipboard_changed),
priv);
} }
static void static void
@ -2327,6 +2342,14 @@ casilda_compositor_unrealize (GtkWidget *widget)
priv->frame_clock = NULL; priv->frame_clock = NULL;
if (priv->clipboard && priv->clipboard_source)
{
g_signal_handler_disconnect (priv->clipboard, priv->clipboard_source);
priv->clipboard_source = 0;
}
priv->clipboard = NULL;
GTK_WIDGET_CLASS (casilda_compositor_parent_class)->unrealize (widget); GTK_WIDGET_CLASS (casilda_compositor_parent_class)->unrealize (widget);
} }
@ -2377,10 +2400,11 @@ seat_request_set_selection (struct wl_listener *listener, void *data)
{ {
CasildaCompositorPrivate *priv = wl_container_of (listener, priv, request_set_selection); CasildaCompositorPrivate *priv = wl_container_of (listener, priv, request_set_selection);
struct wlr_seat_request_set_selection_event *event = data; struct wlr_seat_request_set_selection_event *event = data;
struct wlr_data_source *a;
wlr_seat_set_selection (priv->seat, event->source, event->serial); wlr_seat_set_selection (priv->seat, event->source, event->serial);
/* TODO: integrate with Gtk clipboard */
/* Integrate with Gtk clipboard */
casilda_clipboard_set_from_data_source (priv->clipboard, event->source);
} }
static void static void

View file

@ -26,6 +26,8 @@
#pragma once #pragma once
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <wlr/backend.h>
#include <wlr/types/wlr_seat.h>
G_GNUC_INTERNAL G_GNUC_INTERNAL
void casilda_buffer_configure_shm (struct wl_display *wl_display); void casilda_buffer_configure_shm (struct wl_display *wl_display);
@ -35,3 +37,9 @@ void casilda_buffer_configure_dmabuf (struct wl_display *wl_display);
G_GNUC_INTERNAL G_GNUC_INTERNAL
GdkTexture *casilda_buffer_texture_from_wlr (struct wlr_buffer *buffer, pixman_region32_t *damage, GdkTexture *prev); GdkTexture *casilda_buffer_texture_from_wlr (struct wlr_buffer *buffer, pixman_region32_t *damage, GdkTexture *prev);
G_GNUC_INTERNAL
void casilda_clipboard_seat_set_selection (GdkClipboard *clipboard, struct wlr_seat *seat);
G_GNUC_INTERNAL
void casilda_clipboard_set_from_data_source (GdkClipboard *clipboard, struct wlr_data_source *source);

View file

@ -2,6 +2,7 @@ api_version = '1.0'
casilda_sources = [ casilda_sources = [
'casilda-buffer.c', 'casilda-buffer.c',
'casilda-clipboard.c',
'casilda-compositor.c', 'casilda-compositor.c',
'casilda-wayland-source.c', 'casilda-wayland-source.c',
] ]
@ -27,6 +28,7 @@ casilda_headers += configure_file(
casilda_deps = [ casilda_deps = [
gtk4_dep, gtk4_dep,
gio_unix_dep,
libdrm_dep, libdrm_dep,
wlroots_dep, wlroots_dep,
xkbcommon, xkbcommon,

View file

@ -15,10 +15,14 @@ class MyApplication(Gtk.Application):
self.keyboard = self.window.get_display().get_default_seat().get_keyboard() self.keyboard = self.window.get_display().get_default_seat().get_keyboard()
box = Gtk.Box (orientation=Gtk.Orientation.VERTICAL, spacing=12) box = Gtk.Box (orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.press_label = Gtk.Label(halign=Gtk.Align.START)
self.release_label = Gtk.Label(halign=Gtk.Align.START) box.append(Gtk.Entry())
self.mod_label = Gtk.Label(halign=Gtk.Align.START) box.append(Gtk.Label(label="Some test text you can select to test the clipboard", selectable=True))
self.click_label = Gtk.Label(halign=Gtk.Align.START)
self.press_label = Gtk.Label(halign=Gtk.Align.START, selectable=True)
self.release_label = Gtk.Label(halign=Gtk.Align.START, selectable=True)
self.mod_label = Gtk.Label(halign=Gtk.Align.START, selectable=True)
self.click_label = Gtk.Label(halign=Gtk.Align.START, selectable=True)
box.append(self.press_label) box.append(self.press_label)
box.append(self.release_label) box.append(self.release_label)
@ -43,15 +47,15 @@ class MyApplication(Gtk.Application):
def handle_key_pressed(self, controller, keyval, keycode, state): def handle_key_pressed(self, controller, keyval, keycode, state):
self.press_label.set_text(f"EventControllerKey::key-pressed\n\t{keyval=} {keycode=}\n\t{state=}\n\t{self.keyboard.props.modifier_state=}") self.press_label.set_text(f"EventControllerKey::key-pressed\n\t{keyval=} {keycode=}\n\t{state=}\n\t{self.keyboard.props.modifier_state=}")
return True return False
def handle_key_released(self, controller, keyval, keycode, state): def handle_key_released(self, controller, keyval, keycode, state):
self.release_label.set_text(f"EventControllerKey::key-released\n\t{keyval=} {keycode=}\n\t{state=}\n\t{self.keyboard.props.modifier_state=}") self.release_label.set_text(f"EventControllerKey::key-released\n\t{keyval=} {keycode=}\n\t{state=}\n\t{self.keyboard.props.modifier_state=}")
return True return False
def handle_modifiers(self, controller, state): def handle_modifiers(self, controller, state):
self.mod_label.set_text(f"EventControllerKey::modifiers\n\t{state}\n\t{self.keyboard.props.modifier_state=}") self.mod_label.set_text(f"EventControllerKey::modifiers\n\t{state}\n\t{self.keyboard.props.modifier_state=}")
return True return False
def click_released(self, controller, n, x, y): def click_released(self, controller, n, x, y):
self.click_label.set_text(f"GestureClick::released\n\t{n=} {x=} {y=}\n\t{self.keyboard.props.modifier_state=}") self.click_label.set_text(f"GestureClick::released\n\t{n=} {x=} {y=}\n\t{self.keyboard.props.modifier_state=}")