emscripten: Attempt at adding clipboard support.

This is not usable as-is, do not merge.

Reference Issue #13785.
This commit is contained in:
Ryan C. Gordon 2025-09-25 14:33:25 -04:00
parent f71348f38b
commit 756569f653
No known key found for this signature in database
GPG key ID: FA148B892AB48044
3 changed files with 214 additions and 0 deletions

View file

@ -0,0 +1,164 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_EMSCRIPTEN
#include "SDL_emscriptenvideo.h"
#include "SDL_emscriptenclipboard.h"
#include "../SDL_clipboard_c.h"
#include "../../events/SDL_events_c.h"
void Emscripten_InitClipboard(SDL_VideoDevice *device)
{
MAIN_THREAD_EM_ASM_INT({
// !!! FIXME: we make sure `Module['SDL3']` exists like this all over. Can we move this to the "main" startup code?
if (typeof(Module['SDL3']) === 'undefined') {
Module['SDL3'] = {};
}
var SDL3 = Module['SDL3'];
SDL3.clipboard = {};
SDL3.clipboard.contents = [];
SDL3.clipboard.polling_id = -1;
// we don't touch the clipboard until the app explicitly requests it, because it pops
// up a permissions dialog and might result in ongoing overhead. So we call this
// function when we finally need to make contact with the outside world.
SDL3.clipboard.prepare = function() {
SDL3.clipboard.refresh = function() {
navigator.clipboard.read().then((clipboardItems) => {
SDL3.clipboard.contents = [];
for (clipboardItem of clipboardItems) {
for (type of clipboardItem.types) {
clipboardItem.getType(type).then((blob) => {
blob.arrayBuffer().then((buffer) => {
console.log("Got a new clipboard buffer for type '" + type + "'");
SDL3.clipboard.contents[type] = new Uint8Array(buffer);
});
});
}
}
}).catch((err) => { /*console.error(err);*/ /* oh well, no clipboard update for you. */ });
};
SDL3.clipboard.refresh(); // get this started right now.
// There's a new "clipboardchanged" event proposed (as of September 2025). Try to use it.
if ('onclipboardchange' in navigator.clipboard) {
SDL3.clipboard.eventHandlerClipboardChange = function(event) {
SDL3.clipboard.refresh();
};
navigator.clipboard.addEventListener('clipboardchange', SDL3.clipboard.eventHandlerClipboardChange);
} else { // fall back to polling once per second. Gross.
SDL3.clipboard.polling_id = setInterval(SDL3.clipboard.refresh, 1000);
}
SDL3.clipboard.prepare = function() {}; // turn this into a no-op now that we're ready.
};
});
}
const char **Emscripten_GetTextMimeTypes(SDL_VideoDevice *device, size_t *num_mime_types)
{
static const char *text_plain = "text/plain";
*num_mime_types = 1;
return &text_plain;
}
bool Emscripten_SetClipboardData(SDL_VideoDevice *device)
{
MAIN_THREAD_EM_ASM({ var SDL3 = Module['SDL3']; SDL3.clipboard.contents = []; SDL3.clipboard.pending_contents = []; });
for (size_t i = 0; i < device->num_clipboard_mime_types; i++) {
const char *mime_type = device->clipboard_mime_types[i];
size_t clipboard_data_size = 0;
const void *clipboard_data = device->clipboard_callback(device->clipboard_userdata, mime_type, &clipboard_data_size);
if (clipboard_data && (clipboard_data_size > 0)) {
MAIN_THREAD_EM_ASM({
var mime_type = UTF8ToString($0);
var clipboard_data = $1;
var clipboard_data_size = $2;
var ui8array = new Uint8Array(Module.HEAPU8.subarray(clipboard_data, clipboard_data + clipboard_data_size)); // !!! FIXME: I _think_ this makes a copy of the data, not just a view into the heap.
var SDL3 = Module['SDL3'];
SDL3.clipboard.contents[mime_type] = ui8array;
SDL3.clipboard.pending_contents[mime_type] = new Blob([ui8array], { type: mime_type} );
}, mime_type, clipboard_data, clipboard_data_size);
}
}
MAIN_THREAD_EM_ASM({
var SDL3 = Module['SDL3'];
const clipboardItem = new ClipboardItem(SDL3.clipboard.pending_contents);
SDL3.clipboard.pending_contents = undefined;
navigator.clipboard.write([clipboardItem]).then(() => {
//console.log("We set the clipboard!");
}).catch((err) => { console.error(err); /* oh well, ignore it. */ });
});
return true; // this is an async call, just pretend it worked, even if it didn't.
}
void *Emscripten_GetClipboardData(SDL_VideoDevice *device, const char *mime_type, size_t *length)
{
if (!Emscripten_HasClipboardData(device, mime_type)) {
return NULL;
}
const size_t buflen = (size_t) MAIN_THREAD_EM_ASM_INT({ return Module['SDL3'].clipboard.contents[UTF8ToString($0)].byteLength; }, mime_type);
void *retval = SDL_malloc(buflen + 1);
if (retval) {
MAIN_THREAD_EM_ASM({
var ui8array = Module['SDL3'].clipboard.contents[UTF8ToString($0)];
var heapu8 = new Uint8Array(Module.HEAPU8.buffer, $1, $2);
heapu8.set(ui8array);
}, mime_type, retval, buflen);
((char *)retval)[buflen] = '\0'; // make sure it's null-terminated.
}
return retval;
}
bool Emscripten_HasClipboardData(SDL_VideoDevice *device, const char *mime_type)
{
return MAIN_THREAD_EM_ASM_INT({
var SDL3 = Module['SDL3'];
SDL3.clipboard.prepare();
return (SDL3.clipboard.contents[UTF8ToString($0)] !== undefined) ? 1 : 0;
}, mime_type);
}
void Emscripten_QuitClipboard(SDL_VideoDevice *device)
{
MAIN_THREAD_EM_ASM({
var SDL3 = Module['SDL3'];
if (SDL3.clipboard.eventHandlerClipboardChange !== undefined) {
navigator.clipboard.removeEventListener('clipboardchange', SDL3.clipboard.eventHandlerClipboardChange);
SDL3.clipboard.eventHandlerClipboardChange = undefined;
}
if (SDL3.clipboard.polling_id != -1) {
clearInterval(SDL3.clipboard.polling_id);
}
SDL3.clipboard = undefined;
});
}
#endif // SDL_VIDEO_DRIVER_EMSCRIPTEN

View file

@ -0,0 +1,41 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"
#ifndef SDL_emscriptenclipboard_h_
#define SDL_emscriptenclipboard_h_
typedef struct Emscripten_ClipboardData {
SDL_ClipboardDataCallback callback;
void *userdata;
const char **mime_types;
size_t mime_count;
Uint32 sequence;
} SDLEmscripten_ClipboardData;
extern void Emscripten_InitClipboard(SDL_VideoDevice *_this);
extern const char **Emscripten_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types);
extern bool Emscripten_SetClipboardData(SDL_VideoDevice *_this);
extern void *Emscripten_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *length);
extern bool Emscripten_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type);
extern void Emscripten_QuitClipboard(SDL_VideoDevice *_this);
#endif // SDL_emscriptenclipboard_h_

View file

@ -31,6 +31,7 @@
#include "SDL_emscriptenframebuffer.h"
#include "SDL_emscriptenevents.h"
#include "SDL_emscriptenmouse.h"
#include "SDL_emscriptenclipboard.h"
#define EMSCRIPTENVID_DRIVER_NAME "emscripten"
@ -184,6 +185,11 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void)
device->GL_SwapWindow = Emscripten_GLES_SwapWindow;
device->GL_DestroyContext = Emscripten_GLES_DestroyContext;
device->GetTextMimeTypes = Emscripten_GetTextMimeTypes;
device->SetClipboardData = Emscripten_SetClipboardData;
device->GetClipboardData = Emscripten_GetClipboardData;
device->HasClipboardData = Emscripten_HasClipboardData;
device->free = Emscripten_DeleteDevice;
Emscripten_ListenSystemTheme();
@ -392,6 +398,8 @@ bool Emscripten_VideoInit(SDL_VideoDevice *_this)
Emscripten_RegisterGlobalEventHandlers(_this);
Emscripten_InitClipboard(_this);
// We're done!
return true;
}
@ -407,6 +415,7 @@ static void Emscripten_VideoQuit(SDL_VideoDevice *_this)
Emscripten_UnregisterGlobalEventHandlers(_this);
Emscripten_QuitMouse();
Emscripten_UnlistenSystemTheme();
Emscripten_QuitClipboard(_this);
pumpevents_has_run = false;
pending_swap_interval = -1;
}