clipboard
This commit is contained in:
parent
b5d0f52da0
commit
850ef7bfb3
4 changed files with 325 additions and 10 deletions
|
|
@ -441,6 +441,33 @@ function domStopFrameLoop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Clipboard ────────────────────────────────────────────────────────
|
// ─── Clipboard ────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Read path is necessarily a latched buffer: navigator.clipboard.readText
|
||||||
|
// is async (Promise) but the C++ Clipboard::GetText signature is
|
||||||
|
// synchronous to match the Wayland / Win32 backends. Two feeders keep
|
||||||
|
// the cache fresh:
|
||||||
|
//
|
||||||
|
// 1. A capture-phase `paste` listener on `window` reads
|
||||||
|
// event.clipboardData.getData('text/plain') synchronously — this
|
||||||
|
// covers the standard Ctrl+V flow, where the paste event fires
|
||||||
|
// in the same task as the C++ key handler that will call GetText.
|
||||||
|
// 2. Each GetText call also kicks off a navigator.clipboard.readText
|
||||||
|
// in the background. The promise resolves in a later task; the
|
||||||
|
// first call usually still returns the latched paste value (or
|
||||||
|
// nullopt), subsequent calls see the readText result.
|
||||||
|
|
||||||
|
let __clipboardCache = null;
|
||||||
|
|
||||||
|
window.addEventListener("paste", (event) => {
|
||||||
|
try {
|
||||||
|
const t = event.clipboardData &&
|
||||||
|
event.clipboardData.getData("text/plain");
|
||||||
|
if (typeof t === "string") __clipboardCache = t;
|
||||||
|
} catch (err) {
|
||||||
|
// Some browsers reject clipboardData access inside non-input
|
||||||
|
// targets; the async readText fallback still has a chance.
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
function clipboardSetText(strPtr, strLen) {
|
function clipboardSetText(strPtr, strLen) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -452,6 +479,19 @@ function clipboardSetText(strPtr, strLen) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clipboardGetText() {
|
||||||
|
// Best-effort async refresh. Permission/focus rejections are
|
||||||
|
// expected in non-activated contexts — swallow them so we don't
|
||||||
|
// spam the console on every paste check.
|
||||||
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
|
navigator.clipboard.readText()
|
||||||
|
.then((t) => { if (typeof t === "string") __clipboardCache = t; })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
if (__clipboardCache === null) return 0;
|
||||||
|
return __writeUtf8(__clipboardCache);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── History / routing ────────────────────────────────────────────────
|
// ─── History / routing ────────────────────────────────────────────────
|
||||||
|
|
||||||
function pushState(dataPtr, dataLen, titlePtr, titleLen, urlPtr, urlLen) {
|
function pushState(dataPtr, dataLen, titlePtr, titleLen, urlPtr, urlLen) {
|
||||||
|
|
@ -547,7 +587,7 @@ Object.assign(window.crafter_webbuild_env, {
|
||||||
domStartFrameLoop, domStopFrameLoop,
|
domStartFrameLoop, domStopFrameLoop,
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
clipboardSetText,
|
clipboardSetText, clipboardGetText,
|
||||||
|
|
||||||
// History
|
// History
|
||||||
pushState, addPopStateListener, removePopStateListener, getPathName,
|
pushState, addPopStateListener, removePopStateListener, getPathName,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ module;
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#endif
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
|
@ -36,6 +38,8 @@ using namespace Crafter;
|
||||||
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
namespace {
|
namespace {
|
||||||
|
// ─── write path ──────────────────────────────────────────────────
|
||||||
|
//
|
||||||
// Heap-allocated state attached to each wl_data_source via its
|
// Heap-allocated state attached to each wl_data_source via its
|
||||||
// user_data slot. The text must outlive the source (the compositor
|
// user_data slot. The text must outlive the source (the compositor
|
||||||
// can call `send` minutes after we set the selection, every time a
|
// can call `send` minutes after we set the selection, every time a
|
||||||
|
|
@ -95,6 +99,113 @@ namespace {
|
||||||
.dnd_finished = OnSourceDndFinished,
|
.dnd_finished = OnSourceDndFinished,
|
||||||
.action = OnSourceAction,
|
.action = OnSourceAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── read path ───────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// The compositor announces every clipboard selection (and every
|
||||||
|
// drag-and-drop) via a `wl_data_offer` we have to register a
|
||||||
|
// listener on to learn its advertised mime types. The lifecycle:
|
||||||
|
//
|
||||||
|
// 1. wl_data_device.data_offer → fresh wl_data_offer ID. We
|
||||||
|
// attach an offer listener so we receive the mime list.
|
||||||
|
// 2. wl_data_offer.offer (N×) → one event per advertised mime.
|
||||||
|
// 3. wl_data_device.selection → the offer becomes "the
|
||||||
|
// clipboard". Replaces (and invalidates) the previous one.
|
||||||
|
// An offer parameter of nullptr means the clipboard was
|
||||||
|
// cleared.
|
||||||
|
//
|
||||||
|
// DnD offers travel through the same data_offer event and end up
|
||||||
|
// delivered via `enter`/`leave`/`drop`. We don't implement DnD,
|
||||||
|
// but we still need to destroy those offers so we don't leak the
|
||||||
|
// mime vector attached to them — we stash the most recent DnD
|
||||||
|
// offer on enter and destroy it on leave/drop.
|
||||||
|
struct OfferState {
|
||||||
|
std::vector<std::string> mimes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map keyed on raw wl_data_offer pointer. The compositor hands the
|
||||||
|
// same pointer back in subsequent events, so identity is stable.
|
||||||
|
std::unordered_map<wl_data_offer*, OfferState> g_offers;
|
||||||
|
wl_data_offer* g_selectionOffer = nullptr;
|
||||||
|
wl_data_offer* g_dndOffer = nullptr;
|
||||||
|
bool g_listenerAttached = false;
|
||||||
|
|
||||||
|
void DestroyOffer(wl_data_offer* offer) {
|
||||||
|
if (offer == nullptr) return;
|
||||||
|
g_offers.erase(offer);
|
||||||
|
wl_data_offer_destroy(offer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnOfferOffer(void*, wl_data_offer* offer, const char* mime) {
|
||||||
|
auto it = g_offers.find(offer);
|
||||||
|
if (it != g_offers.end()) it->second.mimes.emplace_back(mime);
|
||||||
|
}
|
||||||
|
void OnOfferSourceActions(void*, wl_data_offer*, std::uint32_t) {}
|
||||||
|
void OnOfferAction(void*, wl_data_offer*, std::uint32_t) {}
|
||||||
|
|
||||||
|
constexpr wl_data_offer_listener kOfferListener = {
|
||||||
|
.offer = OnOfferOffer,
|
||||||
|
.source_actions = OnOfferSourceActions,
|
||||||
|
.action = OnOfferAction,
|
||||||
|
};
|
||||||
|
|
||||||
|
void OnDeviceDataOffer(void*, wl_data_device*, wl_data_offer* offer) {
|
||||||
|
// First event in the sequence — register so we hear the mime list.
|
||||||
|
// The offer can still end up unused (DnD that doesn't enter our
|
||||||
|
// surface) but we always destroy it later in selection or
|
||||||
|
// leave/drop.
|
||||||
|
g_offers.emplace(offer, OfferState{});
|
||||||
|
wl_data_offer_add_listener(offer, &kOfferListener, nullptr);
|
||||||
|
}
|
||||||
|
void OnDeviceEnter(void*, wl_data_device*, std::uint32_t,
|
||||||
|
wl_surface*, wl_fixed_t, wl_fixed_t,
|
||||||
|
wl_data_offer* offer) {
|
||||||
|
// DnD started over our surface. We never accept, so we just
|
||||||
|
// hold the offer until leave/drop and free it then.
|
||||||
|
g_dndOffer = offer;
|
||||||
|
}
|
||||||
|
void OnDeviceLeave(void*, wl_data_device*) {
|
||||||
|
DestroyOffer(g_dndOffer);
|
||||||
|
g_dndOffer = nullptr;
|
||||||
|
}
|
||||||
|
void OnDeviceMotion(void*, wl_data_device*, std::uint32_t,
|
||||||
|
wl_fixed_t, wl_fixed_t) {}
|
||||||
|
void OnDeviceDrop(void*, wl_data_device*) {
|
||||||
|
DestroyOffer(g_dndOffer);
|
||||||
|
g_dndOffer = nullptr;
|
||||||
|
}
|
||||||
|
void OnDeviceSelection(void*, wl_data_device*, wl_data_offer* offer) {
|
||||||
|
// Selection changed; previous selection offer is invalid.
|
||||||
|
// `offer == nullptr` means the clipboard was cleared.
|
||||||
|
DestroyOffer(g_selectionOffer);
|
||||||
|
g_selectionOffer = offer;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr wl_data_device_listener kDeviceListener = {
|
||||||
|
.data_offer = OnDeviceDataOffer,
|
||||||
|
.enter = OnDeviceEnter,
|
||||||
|
.leave = OnDeviceLeave,
|
||||||
|
.motion = OnDeviceMotion,
|
||||||
|
.drop = OnDeviceDrop,
|
||||||
|
.selection = OnDeviceSelection,
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* PickTextMime(const OfferState& s) {
|
||||||
|
// Same priority order we advertise from SetText, so a round-trip
|
||||||
|
// through ourselves picks the richest mime first.
|
||||||
|
constexpr std::array kWantedMimes = {
|
||||||
|
"text/plain;charset=utf-8",
|
||||||
|
"text/plain",
|
||||||
|
"UTF8_STRING",
|
||||||
|
"TEXT",
|
||||||
|
};
|
||||||
|
for (const char* want : kWantedMimes) {
|
||||||
|
for (const auto& m : s.mimes) {
|
||||||
|
if (m == want) return want;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -107,9 +218,24 @@ namespace {
|
||||||
namespace Crafter::DomEnv {
|
namespace Crafter::DomEnv {
|
||||||
__attribute__((import_module("env"), import_name("clipboardSetText")))
|
__attribute__((import_module("env"), import_name("clipboardSetText")))
|
||||||
bool DomClipboardSetText(const char* text, std::int32_t len);
|
bool DomClipboardSetText(const char* text, std::int32_t len);
|
||||||
|
|
||||||
|
// JS returns either 0 (no text latched) or a wasm-malloc'd NUL-
|
||||||
|
// terminated UTF-8 pointer that we own and free. Matches the
|
||||||
|
// existing `getValue` ABI used by HtmlElementPtr::GetValue.
|
||||||
|
__attribute__((import_module("env"), import_name("clipboardGetText")))
|
||||||
|
const char* DomClipboardGetText();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void Crafter::Clipboard::Initialize() {
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
|
if (g_listenerAttached) return;
|
||||||
|
if (Device::dataDevice == nullptr) return; // compositor doesn't expose it
|
||||||
|
wl_data_device_add_listener(Device::dataDevice, &kDeviceListener, nullptr);
|
||||||
|
g_listenerAttached = true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool Crafter::Clipboard::SetText(std::string_view text) {
|
bool Crafter::Clipboard::SetText(std::string_view text) {
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
return Crafter::DomEnv::DomClipboardSetText(text.data(), static_cast<std::int32_t>(text.size()));
|
return Crafter::DomEnv::DomClipboardSetText(text.data(), static_cast<std::int32_t>(text.size()));
|
||||||
|
|
@ -186,3 +312,131 @@ bool Crafter::Clipboard::SetText(std::string_view text) {
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Crafter::Clipboard::GetText() {
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
const char* raw = Crafter::DomEnv::DomClipboardGetText();
|
||||||
|
if (raw == nullptr) return std::nullopt;
|
||||||
|
std::string out(raw);
|
||||||
|
std::free(const_cast<char*>(raw)); // JS side allocated via WasmAlloc/malloc
|
||||||
|
return out;
|
||||||
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WAYLAND)
|
||||||
|
if (g_selectionOffer == nullptr) return std::nullopt;
|
||||||
|
auto it = g_offers.find(g_selectionOffer);
|
||||||
|
if (it == g_offers.end()) return std::nullopt;
|
||||||
|
const char* mime = PickTextMime(it->second);
|
||||||
|
if (mime == nullptr) return std::nullopt; // selection holds no text mime
|
||||||
|
|
||||||
|
int fds[2];
|
||||||
|
if (::pipe(fds) != 0) return std::nullopt;
|
||||||
|
// Read end stays blocking; we drive it with poll() below.
|
||||||
|
|
||||||
|
wl_data_offer_receive(g_selectionOffer, mime, fds[1]);
|
||||||
|
// Flush so the compositor forwards `receive` to the source app.
|
||||||
|
wl_display_flush(Device::display);
|
||||||
|
// Drop our copy of the write end. The source app's copy stays open
|
||||||
|
// until it finishes writing; once it closes, our read sees EOF.
|
||||||
|
::close(fds[1]);
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
const int wlFd = wl_display_get_fd(Device::display);
|
||||||
|
|
||||||
|
// One-second budget. The poll loop also drives the Wayland event
|
||||||
|
// pump so that if *we* are the source of the selection, our own
|
||||||
|
// data_source.send callback can run and write into the pipe — a
|
||||||
|
// plain blocking read on the pipe would deadlock in that case.
|
||||||
|
constexpr int kTimeoutMs = 1000;
|
||||||
|
auto deadline = std::chrono::steady_clock::now()
|
||||||
|
+ std::chrono::milliseconds(kTimeoutMs);
|
||||||
|
|
||||||
|
bool done = false;
|
||||||
|
while (!done) {
|
||||||
|
// Prepare for a non-blocking read on the wayland fd. The
|
||||||
|
// prepare/read pair lets us safely interleave with poll().
|
||||||
|
while (wl_display_prepare_read(Device::display) != 0) {
|
||||||
|
wl_display_dispatch_pending(Device::display);
|
||||||
|
}
|
||||||
|
wl_display_flush(Device::display);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
int remaining = static_cast<int>(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
deadline - now).count());
|
||||||
|
if (remaining < 0) remaining = 0;
|
||||||
|
|
||||||
|
pollfd pfds[2] = {
|
||||||
|
{ fds[0], POLLIN, 0 },
|
||||||
|
{ wlFd, POLLIN, 0 },
|
||||||
|
};
|
||||||
|
const int n = ::poll(pfds, 2, remaining);
|
||||||
|
if (n < 0) {
|
||||||
|
wl_display_cancel_read(Device::display);
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
wl_display_cancel_read(Device::display);
|
||||||
|
break; // timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pfds[1].revents & POLLIN) {
|
||||||
|
wl_display_read_events(Device::display);
|
||||||
|
wl_display_dispatch_pending(Device::display);
|
||||||
|
} else {
|
||||||
|
wl_display_cancel_read(Device::display);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pfds[0].revents & (POLLIN | POLLHUP)) {
|
||||||
|
char buf[4096];
|
||||||
|
while (true) {
|
||||||
|
const ssize_t r = ::read(fds[0], buf, sizeof(buf));
|
||||||
|
if (r > 0) {
|
||||||
|
out.append(buf, static_cast<std::size_t>(r));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == 0) { done = true; break; } // EOF
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
if (errno == EAGAIN) break; // shouldn't happen (blocking), but harmless
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pfds[0].revents & POLLERR) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::close(fds[0]);
|
||||||
|
if (out.empty()) return std::nullopt;
|
||||||
|
return out;
|
||||||
|
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
|
||||||
|
// OS owns the wide-char buffer until we CloseClipboard, so the
|
||||||
|
// copy-out into std::string must happen before we release the lock.
|
||||||
|
if (!OpenClipboard(nullptr)) return std::nullopt;
|
||||||
|
HANDLE h = GetClipboardData(CF_UNICODETEXT);
|
||||||
|
if (h == nullptr) { CloseClipboard(); return std::nullopt; }
|
||||||
|
const wchar_t* src = static_cast<const wchar_t*>(GlobalLock(h));
|
||||||
|
if (src == nullptr) { CloseClipboard(); return std::nullopt; }
|
||||||
|
|
||||||
|
// CF_UNICODETEXT is NUL-terminated; lstrlenW gives us the length
|
||||||
|
// without the terminator. WideCharToMultiByte with length excluding
|
||||||
|
// the terminator produces a length-prefixed UTF-8 buffer we can
|
||||||
|
// size std::string for exactly.
|
||||||
|
const int wlen = lstrlenW(src);
|
||||||
|
std::string out;
|
||||||
|
if (wlen > 0) {
|
||||||
|
const int u8len = WideCharToMultiByte(CP_UTF8, 0, src, wlen,
|
||||||
|
nullptr, 0, nullptr, nullptr);
|
||||||
|
if (u8len > 0) {
|
||||||
|
out.resize(static_cast<std::size_t>(u8len));
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, src, wlen,
|
||||||
|
out.data(), u8len, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobalUnlock(h);
|
||||||
|
CloseClipboard();
|
||||||
|
if (out.empty()) return std::nullopt;
|
||||||
|
return out;
|
||||||
|
#else
|
||||||
|
return std::nullopt;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ module Crafter.Graphics:Device_impl;
|
||||||
import :Device;
|
import :Device;
|
||||||
import :Window;
|
import :Window;
|
||||||
import :Types;
|
import :Types;
|
||||||
|
import :Clipboard;
|
||||||
import std;
|
import std;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
|
|
@ -418,6 +419,12 @@ void Device::Initialize() {
|
||||||
if (shm == NULL || compositor == NULL || xdgWmBase == NULL) {
|
if (shm == NULL || compositor == NULL || xdgWmBase == NULL) {
|
||||||
throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support");
|
throw std::runtime_error("No wl_shm, wl_compositor or xdg_wm_base support");
|
||||||
}
|
}
|
||||||
|
// After the registry roundtrip the data_device (if the compositor
|
||||||
|
// exposes one) is bound. Clipboard::Initialize attaches the
|
||||||
|
// selection listener that Clipboard::GetText reads from; doing it
|
||||||
|
// before the first wl_data_device.selection arrives is what lets
|
||||||
|
// GetText work the instant a frame is rendered.
|
||||||
|
Clipboard::Initialize();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
export module Crafter.Graphics:Clipboard;
|
export module Crafter.Graphics:Clipboard;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
// Native system-clipboard writes. No popen, no helper binaries — just
|
// Native system-clipboard reads and writes. No popen, no helper
|
||||||
// the platform's own clipboard API. Implementation lives next to the
|
// binaries — just the platform's own clipboard API. Implementation
|
||||||
// other window-backend code (Wayland data_device on Linux, Win32 in
|
// lives next to the other window-backend code (Wayland data_device on
|
||||||
// the Windows build); callers don't pick a backend.
|
// Linux, Win32 in the Windows build, navigator.clipboard +
|
||||||
//
|
// `paste` event in the DOM build); callers don't pick a backend.
|
||||||
// `Get` is intentionally not exposed yet: paste-from-clipboard isn't
|
|
||||||
// a feature the game's UI wants right now, and the read path needs
|
|
||||||
// more lifecycle plumbing (mime negotiation, fd reads on the Wayland
|
|
||||||
// event loop) than the simple write path. Easy to add later.
|
|
||||||
export namespace Crafter::Clipboard {
|
export namespace Crafter::Clipboard {
|
||||||
|
|
||||||
|
// One-time backend setup. Currently only the Wayland path uses it
|
||||||
|
// (attaches a wl_data_device listener so we observe selection
|
||||||
|
// offers from other apps). Win32 and the DOM build are no-ops.
|
||||||
|
// Called from Device::Initialize after the Wayland registry
|
||||||
|
// roundtrip — calling it again is harmless.
|
||||||
|
void Initialize();
|
||||||
|
|
||||||
// Place `text` on the system clipboard as UTF-8 plain text. Returns
|
// Place `text` on the system clipboard as UTF-8 plain text. Returns
|
||||||
// true if the platform accepted the request — false means the
|
// true if the platform accepted the request — false means the
|
||||||
// backend isn't initialised, no input event has been seen yet
|
// backend isn't initialised, no input event has been seen yet
|
||||||
|
|
@ -39,4 +42,15 @@ export namespace Crafter::Clipboard {
|
||||||
// either another app replaces the selection or the application
|
// either another app replaces the selection or the application
|
||||||
// exits; the caller doesn't need to keep `text` alive.
|
// exits; the caller doesn't need to keep `text` alive.
|
||||||
bool SetText(std::string_view text);
|
bool SetText(std::string_view text);
|
||||||
|
|
||||||
|
// Read the current clipboard contents as UTF-8 plain text. Returns
|
||||||
|
// nullopt if the clipboard is empty, holds no text-typed payload,
|
||||||
|
// the platform read failed, or — on the DOM build — the latched
|
||||||
|
// buffer is empty because the user hasn't pasted yet and
|
||||||
|
// navigator.clipboard.readText() has not resolved. The Wayland
|
||||||
|
// path negotiates one of the standard text mime types in order
|
||||||
|
// (text/plain;charset=utf-8, text/plain, UTF8_STRING, TEXT) and
|
||||||
|
// pumps the wl_display event loop while reading so the call also
|
||||||
|
// works when the source of the selection is our own process.
|
||||||
|
std::optional<std::string> GetText();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue