/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® Catcrafts.net 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; either version 3.0 of the License, or (at your option) any later version. This 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 */ module; #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include #include #include #include #endif #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #endif module Crafter.Graphics; import std; using namespace Crafter; #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND namespace { // Heap-allocated state attached to each wl_data_source via its // user_data slot. The text must outlive the source (the compositor // can call `send` minutes after we set the selection, every time a // remote app pastes), so we own it here and free it from // `OnSourceCancelled` — which is the only callback the compositor // guarantees will fire when the source is no longer needed (when // the selection is replaced, or on app exit). struct Held { std::string text; }; void OnSourceTarget(void*, wl_data_source*, const char*) {} void OnSourceSend(void* data, wl_data_source*, const char* /*mime_type*/, std::int32_t fd) { // We only ever advertise text MIME types, so any negotiated // type maps to the same UTF-8 buffer. `send` may fire multiple // times across the lifetime of one selection (each paste is a // fresh fd), so we must not consume `text` here. Held* h = static_cast(data); const char* p = h->text.data(); std::size_t rem = h->text.size(); while (rem > 0) { const ssize_t w = ::write(fd, p, rem); if (w < 0) { if (errno == EINTR) continue; break; // pipe closed; remote gave up reading } if (w == 0) break; p += w; rem -= static_cast(w); } ::close(fd); } void OnSourceCancelled(void* data, wl_data_source* source) { // Selection was replaced (someone else copied) or the app is // exiting. Either way, this is our hook to release the held // text and the source itself. delete static_cast(data); wl_data_source_destroy(source); } // Drag-and-drop only callbacks. We never start a DnD action, so // these can't fire — but the listener struct's size is fixed at // the v3 shape, and wayland-client uses the struct's slots // directly. Stubs are required. void OnSourceDndDropPerformed(void*, wl_data_source*) {} void OnSourceDndFinished(void*, wl_data_source*) {} void OnSourceAction(void*, wl_data_source*, std::uint32_t) {} constexpr wl_data_source_listener kSourceListener = { .target = OnSourceTarget, .send = OnSourceSend, .cancelled = OnSourceCancelled, .dnd_drop_performed = OnSourceDndDropPerformed, .dnd_finished = OnSourceDndFinished, .action = OnSourceAction, }; } #endif bool Crafter::Clipboard::SetText(std::string_view text) { #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) { // Compositor doesn't expose wl_data_device_manager (rare; some // headless / minimal compositors). Caller can fall back. return false; } // wl_data_device.set_selection requires a serial from a recent // input event. We track the most recent pointer-enter serial on // each window — fine for "user clicked Copy" interactions, since // the click itself produced the serial. Without one, the // compositor would silently reject the request. std::uint32_t serial = 0; if (Device::focusedWindow != nullptr) { serial = Device::focusedWindow->lastPointerSerial_; } if (serial == 0) return false; auto* held = new Held{ std::string(text) }; wl_data_source* source = wl_data_device_manager_create_data_source(Device::dataDeviceManager); if (source == nullptr) { delete held; return false; } wl_data_source_add_listener(source, &kSourceListener, held); // Advertise the four common text MIME types so legacy + modern // pasters both find a match. Wayland clients prefer the first one // they recognise, in order. wl_data_source_offer(source, "text/plain;charset=utf-8"); wl_data_source_offer(source, "text/plain"); wl_data_source_offer(source, "UTF8_STRING"); wl_data_source_offer(source, "TEXT"); wl_data_device_set_selection(Device::dataDevice, source, serial); // Push the request so the compositor sees it before the next event // loop iteration — otherwise a quick read in another app might // miss the selection update. wl_display_flush(Device::display); return true; #elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32) // CF_UNICODETEXT round-trip. Convert UTF-8 → UTF-16, allocate a // moveable HGLOBAL, hand it off to the OS. The OS frees the global // when the next clipboard owner replaces the data, so our caller // doesn't need to keep `text` alive — same lifetime contract as // the Wayland path. if (!OpenClipboard(nullptr)) return false; EmptyClipboard(); const int wlen = MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast(text.size()), nullptr, 0); if (wlen < 0) { CloseClipboard(); return false; } HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, (static_cast(wlen) + 1) * sizeof(wchar_t)); if (h == nullptr) { CloseClipboard(); return false; } wchar_t* dst = static_cast(GlobalLock(h)); if (dst == nullptr) { GlobalFree(h); CloseClipboard(); return false; } MultiByteToWideChar(CP_UTF8, 0, text.data(), static_cast(text.size()), dst, wlen); dst[wlen] = L'\0'; GlobalUnlock(h); const bool ok = SetClipboardData(CF_UNICODETEXT, h) != nullptr; if (!ok) GlobalFree(h); // OS only takes ownership on success CloseClipboard(); return ok; #else (void)text; return false; #endif }