Files

238 lines
9.9 KiB
C++

/*
* transfer.hpp
* Multi-format clipboard, drag and drop, and window server requests for
* MontaukOS GUI apps (the raw ABI is "Window server messaging" in
* Api/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
namespace gui {
using montauk::abi::TransferItem;
using montauk::abi::TransferFormatInfo;
// ---- Building a transfer ----------------------------------------------
// Collects the formats of one clipboard write or drag. Holds pointers
// only: the strings and buffers passed to add() must outlive the call
// that hands the transfer to the kernel (which copies them).
struct TransferBuilder {
TransferItem items[montauk::abi::TRANSFER_MAX_FORMATS];
uint32_t count = 0;
bool add(const char* mime, const void* data, uint32_t len) {
if (count >= montauk::abi::TRANSFER_MAX_FORMATS || !mime) return false;
items[count].mime = (uint64_t)(uintptr_t)mime;
items[count].data = (uint64_t)(uintptr_t)data;
items[count].len = len;
items[count]._reserved = 0;
count++;
return true;
}
bool add_text(const char* mime, const char* text) {
return add(mime, text, text ? (uint32_t)montauk::slen(text) : 0);
}
};
// ---- Clipboard ---------------------------------------------------------
inline bool clipboard_set(const TransferBuilder& transfer) {
return montauk::clipboard_set_data(transfer.items, transfer.count) == 0;
}
inline bool clipboard_has_format(const char* mime) {
uint32_t len = 0;
int rc = montauk::clipboard_get_data(mime, nullptr, 0, &len);
return rc == 0 || montauk::StatusFromResult(rc) == montauk::BufferTooSmall;
}
inline uint64_t clipboard_serial() {
uint64_t serial = 0;
montauk::clipboard_formats(nullptr, 0, &serial);
return serial;
}
// Read a whole format into a new NUL-terminated buffer (montauk::mfree
// it). Returns nullptr if the format is absent.
inline char* clipboard_get_alloc(const char* mime, uint32_t* out_len = nullptr) {
if (out_len) *out_len = 0;
// The clipboard may change between asking for the size and the
// copy; retry a few times rather than return a torn read.
for (int attempt = 0; attempt < 4; attempt++) {
uint32_t len = 0;
int rc = montauk::clipboard_get_data(mime, nullptr, 0, &len);
if (rc < 0 && montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr;
char* buf = (char*)montauk::malloc(len + 1);
if (!buf) return nullptr;
uint32_t got = 0;
rc = montauk::clipboard_get_data(mime, buf, len, &got);
if (rc == 0 && got <= len) {
buf[got] = '\0';
if (out_len) *out_len = got;
return buf;
}
montauk::mfree(buf);
if (montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr;
}
return nullptr;
}
// ---- Drag and drop -----------------------------------------------------
// Start a drag from window `win_id` (call it on the mouse move that
// crosses the drag threshold). Returns the drag serial, or 0 on failure.
// The compositor then shows the drag, sends DRAG_ENTER/OVER/LEAVE and
// DROP to the windows under the pointer, and DRAG_END back to this one.
inline uint32_t drag_begin(int win_id, const TransferBuilder& transfer, uint8_t actions,
uint32_t item_count, const char* label) {
montauk::abi::WinDragBegin info = {};
info.actions = actions;
info.itemCount = item_count;
if (label) montauk::strncpy(info.label, label, sizeof(info.label));
int rc = montauk::win_drag_begin(win_id, &info, transfer.items, transfer.count);
return rc > 0 ? (uint32_t)rc : 0;
}
inline bool drag_has_format(uint32_t serial, const char* mime) {
uint32_t len = 0;
int rc = montauk::win_drag_get_data(serial, mime, nullptr, 0, &len);
return rc == 0 || montauk::StatusFromResult(rc) == montauk::BufferTooSmall;
}
// Read a whole drag format into a new NUL-terminated buffer (montauk::mfree
// it). Allowed once this window has received a drag event for `serial`.
inline char* drag_get_alloc(uint32_t serial, const char* mime, uint32_t* out_len = nullptr) {
if (out_len) *out_len = 0;
uint32_t len = 0;
int rc = montauk::win_drag_get_data(serial, mime, nullptr, 0, &len);
if (rc < 0 && montauk::StatusFromResult(rc) != montauk::BufferTooSmall) return nullptr;
char* buf = (char*)montauk::malloc(len + 1);
if (!buf) return nullptr;
uint32_t got = 0;
rc = montauk::win_drag_get_data(serial, mime, buf, len, &got);
if (rc != 0 || got > len) {
montauk::mfree(buf);
return nullptr;
}
buf[got] = '\0';
if (out_len) *out_len = got;
return buf;
}
// The conventional modifier mapping for a drop: Ctrl copies, Shift moves,
// otherwise `preferred` (e.g. move within a volume, copy across them).
// Falls back to whatever the source offers. 0 if nothing is possible.
inline uint8_t drag_pick_action(uint8_t offered, uint8_t modifiers, uint8_t preferred) {
using namespace montauk::abi;
uint8_t want = preferred;
if (modifiers & WIN_MOD_CTRL) want = WIN_DRAG_COPY;
else if (modifiers & WIN_MOD_SHIFT) want = WIN_DRAG_MOVE;
if (offered & want) return want;
if (offered & WIN_DRAG_COPY) return WIN_DRAG_COPY;
if (offered & WIN_DRAG_MOVE) return WIN_DRAG_MOVE;
if (offered & WIN_DRAG_LINK) return WIN_DRAG_LINK;
return 0;
}
// ---- File lists (TRANSFER_MIME_FILE_LIST) ------------------------------
// Calls fn(path) for each non-empty line of a file list, in place
// (temporarily NUL-terminating each line). Returns the number of paths.
template <typename Fn>
inline int file_list_each(char* list, Fn fn) {
int n = 0;
char* p = list;
while (p && *p) {
char* end = p;
while (*end && *end != '\n') end++;
char saved = *end;
*end = '\0';
char* line = p;
int len = (int)(end - p);
if (len > 0 && line[len - 1] == '\r') line[len - 1] = '\0';
if (line[0]) { fn((const char*)line); n++; }
*end = saved;
p = saved ? end + 1 : end;
}
return n;
}
// ---- Window server requests ---------------------------------------------
inline bool win_request_send(int win_id, uint16_t type, const void* payload, uint16_t size) {
if (size > montauk::abi::WIN_REQUEST_PAYLOAD_MAX) return false;
montauk::abi::WinRequest req = {};
req.type = type;
req.size = size;
req.windowId = win_id;
if (payload && size) montauk::memcpy(req.payload, payload, size);
return montauk::win_request(win_id, &req) == 0;
}
// Ask the desktop to open a catalog item (app manifest id or a built-in
// such as "desktop-settings") as if the user had launched it.
inline bool win_request_launch_item(int win_id, const char* item_id, const char* args = nullptr) {
montauk::abi::WinReqLaunchItem p = {};
montauk::strncpy(p.itemId, item_id ? item_id : "", sizeof(p.itemId));
if (args) montauk::strncpy(p.args, args, sizeof(p.args));
return win_request_send(win_id, montauk::abi::WIN_REQ_LAUNCH_ITEM, &p, sizeof(p));
}
// Declare `win_id` a dialog of `parent_id` (same process): the desktop
// centres it over the parent.
inline bool win_request_set_parent(int win_id, int parent_id) {
montauk::abi::WinReqSetParent p = { parent_id };
return win_request_send(win_id, montauk::abi::WIN_REQ_SET_PARENT, &p, sizeof(p));
}
// Bring one of this app's windows to the front (e.g. re-show an open
// dialog instead of opening a second one).
inline bool win_request_activate(int win_id) {
return win_request_send(win_id, montauk::abi::WIN_REQ_ACTIVATE, nullptr, 0);
}
// Answer DRAG_ENTER/DRAG_OVER with what a drop here would do (0 = refuse).
inline bool win_request_drag_status(int win_id, uint32_t serial, uint8_t action) {
montauk::abi::WinReqDragStatus p = {};
p.serial = serial;
p.action = action;
return win_request_send(win_id, montauk::abi::WIN_REQ_DRAG_STATUS, &p, sizeof(p));
}
// ---- File drops for document apps ----------------------------------------
// For document apps that open a dropped file. Call it for every window
// event; it answers drag events (copy, if the drag carries files and
// `accept` is true) and returns true on a drop, with the first dropped
// path in out_path. Anything else returns false and can be handled as
// usual.
inline bool handle_file_drop(int win_id, const montauk::abi::WinEvent& ev,
char* out_path, int out_max, bool accept = true) {
using namespace montauk::abi;
if (ev.type == WIN_EVENT_DRAG_ENTER || ev.type == WIN_EVENT_DRAG_OVER) {
bool files = accept && drag_has_format(ev.drag.serial, TRANSFER_MIME_FILE_LIST);
win_request_drag_status(win_id, ev.drag.serial, files ? WIN_DRAG_COPY : 0);
return false;
}
if (ev.type != WIN_EVENT_DROP || !accept || !out_path || out_max <= 0) return false;
char* list = drag_get_alloc(ev.drag.serial, TRANSFER_MIME_FILE_LIST);
if (!list) return false;
bool got = false;
file_list_each(list, [&](const char* path) {
if (got) return;
montauk::strncpy(out_path, path, out_max);
got = true;
});
montauk::mfree(list);
return got;
}
} // namespace gui